From bff5b3bf4455de3260642dded4655a41fa19d093 Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Tue, 17 Dec 2024 16:31:08 +0800 Subject: [PATCH 01/44] fix: air-conditioner switch on --- custom_components/xiaomi_home/climate.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index 860afac..baedf61 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -254,6 +254,7 @@ async def async_turn_off(self) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" + # set air-conditioner off if hvac_mode == HVACMode.OFF and self._prop_on: if not await self.set_property_async( prop=self._prop_on, value=False): @@ -261,6 +262,11 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: f'set climate prop.on failed, {hvac_mode}, ' f'{self.entity_id}') return + # set air-conditioner on + if hvac_mode != HVACMode.OFF and not self.get_prop_value( + prop=self._prop_on): + await self.async_turn_on() + # set mode mode_value = self.get_map_value( map_=self._hvac_mode_map, description=hvac_mode) if ( From dbf943386ef5f2ec643c1d404b85deafd4e7650c Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Tue, 17 Dec 2024 17:33:28 +0800 Subject: [PATCH 02/44] fix: set prop --- custom_components/xiaomi_home/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index baedf61..0b1ffd7 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -265,7 +265,7 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: # set air-conditioner on if hvac_mode != HVACMode.OFF and not self.get_prop_value( prop=self._prop_on): - await self.async_turn_on() + await self.set_property_async(prop=self._prop_on, value=True) # set mode mode_value = self.get_map_value( map_=self._hvac_mode_map, description=hvac_mode) From da58d4c0a59aa52e8ff52cc334b9b4f3d1a0c8e5 Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Tue, 17 Dec 2024 17:50:08 +0800 Subject: [PATCH 03/44] perf: remove permanent true statement --- custom_components/xiaomi_home/climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index 0b1ffd7..a1d2ad4 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -255,7 +255,7 @@ async def async_turn_off(self) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" # set air-conditioner off - if hvac_mode == HVACMode.OFF and self._prop_on: + if hvac_mode == HVACMode.OFF: if not await self.set_property_async( prop=self._prop_on, value=False): raise RuntimeError( @@ -263,7 +263,7 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: f'{self.entity_id}') return # set air-conditioner on - if hvac_mode != HVACMode.OFF and not self.get_prop_value( + elif not self.get_prop_value( prop=self._prop_on): await self.set_property_async(prop=self._prop_on, value=True) # set mode @@ -372,7 +372,7 @@ def current_humidity(self) -> Optional[int]: @ property def hvac_mode(self) -> Optional[HVACMode]: """Return the hvac mode. e.g., heat, cool mode.""" - if self._prop_on and self.get_prop_value(prop=self._prop_on) is False: + if not self.get_prop_value(prop=self._prop_on): return HVACMode.OFF return self.get_map_description( map_=self._hvac_mode_map, From b7fc534a34e518a3f05ee1a0049342b21335cfb6 Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Tue, 17 Dec 2024 17:57:02 +0800 Subject: [PATCH 04/44] fix: bool value false --- custom_components/xiaomi_home/climate.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index a1d2ad4..cc12373 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -263,8 +263,7 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: f'{self.entity_id}') return # set air-conditioner on - elif not self.get_prop_value( - prop=self._prop_on): + elif self.get_prop_value(prop=self._prop_on) is False: await self.set_property_async(prop=self._prop_on, value=True) # set mode mode_value = self.get_map_value( @@ -372,7 +371,7 @@ def current_humidity(self) -> Optional[int]: @ property def hvac_mode(self) -> Optional[HVACMode]: """Return the hvac mode. e.g., heat, cool mode.""" - if not self.get_prop_value(prop=self._prop_on): + if self.get_prop_value(prop=self._prop_on) is False: return HVACMode.OFF return self.get_map_description( map_=self._hvac_mode_map, From bf435374d22130b254e74f5b9bf0d6a0339d5b97 Mon Sep 17 00:00:00 2001 From: topsworld Date: Tue, 17 Dec 2024 20:50:01 +0800 Subject: [PATCH 05/44] fix: water heater entity add STATE_OFF --- custom_components/xiaomi_home/water_heater.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/custom_components/xiaomi_home/water_heater.py b/custom_components/xiaomi_home/water_heater.py index 1828316..bcf158d 100644 --- a/custom_components/xiaomi_home/water_heater.py +++ b/custom_components/xiaomi_home/water_heater.py @@ -53,6 +53,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.components.water_heater import ( + STATE_OFF, ATTR_TEMPERATURE, WaterHeaterEntity, WaterHeaterEntityFeature @@ -116,6 +117,7 @@ def __init__( if isinstance(prop.value_range, dict): self._attr_min_temp = prop.value_range['min'] self._attr_max_temp = prop.value_range['max'] + self._attr_precision = prop.value_range['step'] if ( self._attr_temperature_unit is None and prop.external_unit @@ -149,6 +151,7 @@ def __init__( self._attr_supported_features |= ( WaterHeaterEntityFeature.OPERATION_MODE) self._prop_mode = prop + self._attr_operation_list.append(STATE_OFF) async def async_turn_on(self) -> None: """Turn the water heater on.""" @@ -167,6 +170,12 @@ async def async_set_operation_mode(self, operation_mode: str) -> None: """Set the operation mode of the water heater. Must be in the operation_list. """ + if operation_mode == STATE_OFF: + await self.set_property_async(prop=self._prop_on, value=False) + return + if self.get_prop_value(prop=self._prop_on) is False: + await self.set_property_async( + prop=self._prop_on, value=True, update=False) await self.set_property_async( prop=self._prop_mode, value=self.__get_mode_value(description=operation_mode)) @@ -188,6 +197,8 @@ def target_temperature(self) -> Optional[float]: @property def current_operation(self) -> Optional[str]: """Return the current mode.""" + if self.get_prop_value(prop=self._prop_on) is False: + return STATE_OFF return self.__get_mode_description( key=self.get_prop_value(prop=self._prop_mode)) From 1fd5e8e57d5e54e1de3f7b70e2c07135fedc4943 Mon Sep 17 00:00:00 2001 From: topsworld Date: Tue, 17 Dec 2024 21:01:56 +0800 Subject: [PATCH 06/44] fix: set the temperature limit --- custom_components/xiaomi_home/water_heater.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/xiaomi_home/water_heater.py b/custom_components/xiaomi_home/water_heater.py index bcf158d..a6217ae 100644 --- a/custom_components/xiaomi_home/water_heater.py +++ b/custom_components/xiaomi_home/water_heater.py @@ -115,9 +115,6 @@ def __init__( # temperature if prop.name == 'temperature': if isinstance(prop.value_range, dict): - self._attr_min_temp = prop.value_range['min'] - self._attr_max_temp = prop.value_range['max'] - self._attr_precision = prop.value_range['step'] if ( self._attr_temperature_unit is None and prop.external_unit @@ -130,6 +127,9 @@ def __init__( self.entity_id) # target-temperature if prop.name == 'target-temperature': + self._attr_min_temp = prop.value_range['min'] + self._attr_max_temp = prop.value_range['max'] + self._attr_precision = prop.value_range['step'] if self._attr_temperature_unit is None and prop.external_unit: self._attr_temperature_unit = prop.external_unit self._attr_supported_features |= ( From 189060dd864e83b476f03edd8e2fb217f9ab6012 Mon Sep 17 00:00:00 2001 From: topsworld Date: Tue, 17 Dec 2024 21:30:55 +0800 Subject: [PATCH 07/44] fix: add STATE_ON to devices that not define mode --- custom_components/xiaomi_home/water_heater.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/custom_components/xiaomi_home/water_heater.py b/custom_components/xiaomi_home/water_heater.py index a6217ae..ee6a91d 100644 --- a/custom_components/xiaomi_home/water_heater.py +++ b/custom_components/xiaomi_home/water_heater.py @@ -53,6 +53,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.components.water_heater import ( + STATE_ON, STATE_OFF, ATTR_TEMPERATURE, WaterHeaterEntity, @@ -151,6 +152,8 @@ def __init__( self._attr_supported_features |= ( WaterHeaterEntityFeature.OPERATION_MODE) self._prop_mode = prop + if not self._attr_operation_list: + self._attr_operation_list = [STATE_ON] self._attr_operation_list.append(STATE_OFF) async def async_turn_on(self) -> None: @@ -173,6 +176,9 @@ async def async_set_operation_mode(self, operation_mode: str) -> None: if operation_mode == STATE_OFF: await self.set_property_async(prop=self._prop_on, value=False) return + if operation_mode == STATE_ON: + await self.set_property_async(prop=self._prop_on, value=True) + return if self.get_prop_value(prop=self._prop_on) is False: await self.set_property_async( prop=self._prop_on, value=True, update=False) @@ -199,6 +205,8 @@ def current_operation(self) -> Optional[str]: """Return the current mode.""" if self.get_prop_value(prop=self._prop_on) is False: return STATE_OFF + if not self._prop_mode and self.get_prop_value(prop=self._prop_on): + return STATE_ON return self.__get_mode_description( key=self.get_prop_value(prop=self._prop_mode)) From 99e654f0c7285ab959980a1c36a272192afe2604 Mon Sep 17 00:00:00 2001 From: topsworld Date: Wed, 18 Dec 2024 10:34:07 +0800 Subject: [PATCH 08/44] style: remove invalid space --- custom_components/xiaomi_home/climate.py | 14 +++++++------- custom_components/xiaomi_home/config_flow.py | 4 ++-- custom_components/xiaomi_home/light.py | 4 ++-- custom_components/xiaomi_home/miot/miot_client.py | 2 +- custom_components/xiaomi_home/miot/miot_lan.py | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index cc12373..909fee4 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -344,31 +344,31 @@ async def async_set_fan_mode(self, fan_mode): f'set climate prop.fan_mode failed, {fan_mode}, ' f'{self.entity_id}') - @ property + @property def target_temperature(self) -> Optional[float]: """Return the target temperature.""" return self.get_prop_value( prop=self._prop_target_temp) if self._prop_target_temp else None - @ property + @property def target_humidity(self) -> Optional[int]: """Return the target humidity.""" return self.get_prop_value( prop=self._prop_target_humi) if self._prop_target_humi else None - @ property + @property def current_temperature(self) -> Optional[float]: """Return the current temperature.""" return self.get_prop_value( prop=self._prop_env_temp) if self._prop_env_temp else None - @ property + @property def current_humidity(self) -> Optional[int]: """Return the current humidity.""" return self.get_prop_value( prop=self._prop_env_humi) if self._prop_env_humi else None - @ property + @property def hvac_mode(self) -> Optional[HVACMode]: """Return the hvac mode. e.g., heat, cool mode.""" if self.get_prop_value(prop=self._prop_on) is False: @@ -377,7 +377,7 @@ def hvac_mode(self) -> Optional[HVACMode]: map_=self._hvac_mode_map, key=self.get_prop_value(prop=self._prop_mode)) - @ property + @property def fan_mode(self) -> Optional[str]: """Return the fan mode. @@ -387,7 +387,7 @@ def fan_mode(self) -> Optional[str]: map_=self._fan_mode_map, key=self.get_prop_value(prop=self._prop_fan_level)) - @ property + @property def swing_mode(self) -> Optional[str]: """Return the swing mode. diff --git a/custom_components/xiaomi_home/config_flow.py b/custom_components/xiaomi_home/config_flow.py index ccc91f9..c1e4e67 100644 --- a/custom_components/xiaomi_home/config_flow.py +++ b/custom_components/xiaomi_home/config_flow.py @@ -564,8 +564,8 @@ async def display_device_filter_form(self, reason: str): last_step=False, ) - @ staticmethod - @ callback + @staticmethod + @callback def async_get_options_flow( config_entry: config_entries.ConfigEntry, ) -> config_entries.OptionsFlow: diff --git a/custom_components/xiaomi_home/light.py b/custom_components/xiaomi_home/light.py index 610882e..49229f5 100644 --- a/custom_components/xiaomi_home/light.py +++ b/custom_components/xiaomi_home/light.py @@ -236,7 +236,7 @@ def color_temp_kelvin(self) -> Optional[int]: """Return the color temperature.""" return self.get_prop_value(prop=self._prop_color_temp) - @ property + @property def rgb_color(self) -> Optional[tuple[int, int, int]]: """Return the rgb color value.""" rgb = self.get_prop_value(prop=self._prop_color) @@ -247,7 +247,7 @@ def rgb_color(self) -> Optional[tuple[int, int, int]]: b = rgb & 0xFF return r, g, b - @ property + @property def effect(self) -> Optional[str]: """Return the current mode.""" return self.__get_mode_description( diff --git a/custom_components/xiaomi_home/miot/miot_client.py b/custom_components/xiaomi_home/miot/miot_client.py index 771de41..31c87f0 100644 --- a/custom_components/xiaomi_home/miot/miot_client.py +++ b/custom_components/xiaomi_home/miot/miot_client.py @@ -1760,7 +1760,7 @@ def __request_show_devices_changed_notify( delay_sec, self.__show_devices_changed_notify) -@ staticmethod +@staticmethod async def get_miot_instance_async( hass: HomeAssistant, entry_id: str, entry_data: Optional[dict] = None, persistent_notify: Optional[Callable[[str, str, str], None]] = None diff --git a/custom_components/xiaomi_home/miot/miot_lan.py b/custom_components/xiaomi_home/miot/miot_lan.py index 4635572..525be2f 100644 --- a/custom_components/xiaomi_home/miot/miot_lan.py +++ b/custom_components/xiaomi_home/miot/miot_lan.py @@ -564,11 +564,11 @@ def __init__( 0, lambda: self._main_loop.create_task( self.init_async())) - @ property + @property def virtual_did(self) -> str: return self._virtual_did - @ property + @property def mev(self) -> MIoTEventLoop: return self._mev From b5f9e931b70cf4013eebc1aa83923d7ef92d7cdd Mon Sep 17 00:00:00 2001 From: topsworld Date: Wed, 18 Dec 2024 10:35:59 +0800 Subject: [PATCH 09/44] feat: support xiaomi heater devices --- custom_components/xiaomi_home/climate.py | 112 +++++++++++++++++- .../xiaomi_home/miot/specs/specv2entity.py | 27 ++++- 2 files changed, 135 insertions(+), 4 deletions(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index 909fee4..ccda2b6 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -82,9 +82,12 @@ async def async_setup_entry( new_entities = [] for miot_device in device_list: - for data in miot_device.entity_list.get('climate', []): + for data in miot_device.entity_list.get('air-conditioner', []): new_entities.append( AirConditioner(miot_device=miot_device, entity_data=data)) + for data in miot_device.entity_list.get('heater', []): + new_entities.append( + Heater(miot_device=miot_device, entity_data=data)) if new_entities: async_add_entities(new_entities) @@ -115,7 +118,7 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity): def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: - """Initialize the Climate.""" + """Initialize the Air conditioner.""" super().__init__(miot_device=miot_device, entity_data=entity_data) self._attr_icon = 'mdi:air-conditioner' self._attr_supported_features = ClimateEntityFeature(0) @@ -473,3 +476,108 @@ def __ac_state_changed(self, prop: MIoTSpecProperty, value: any) -> None: self._value_ac_state.update(v_ac_state) _LOGGER.debug( 'ac_state update, %s', self._value_ac_state) + + +class Heater(MIoTServiceEntity, ClimateEntity): + """Heater entities for Xiaomi Home.""" + # service: heater + _prop_on: Optional[MIoTSpecProperty] + _prop_mode: Optional[MIoTSpecProperty] + _prop_target_temp: Optional[MIoTSpecProperty] + # service: environment + _prop_env_temp: Optional[MIoTSpecProperty] + _prop_env_humi: Optional[MIoTSpecProperty] + + def __init__( + self, miot_device: MIoTDevice, entity_data: MIoTEntityData + ) -> None: + """Initialize the Heater.""" + super().__init__(miot_device=miot_device, entity_data=entity_data) + self._attr_icon = 'mdi:air-conditioner' + self._attr_supported_features = ClimateEntityFeature(0) + + self._prop_on = None + self._prop_mode = None + self._prop_target_temp = None + self._prop_env_temp = None + self._prop_env_humi = None + + # properties + for prop in entity_data.props: + if prop.name == 'on': + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_ON) + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF) + self._prop_on = prop + elif prop.name == 'target-temperature': + if not isinstance(prop.value_range, dict): + _LOGGER.error( + 'invalid target-temperature value_range format, %s', + self.entity_id) + continue + self._attr_min_temp = prop.value_range['min'] + self._attr_max_temp = prop.value_range['max'] + self._attr_target_temperature_step = prop.value_range['step'] + self._attr_temperature_unit = prop.external_unit + self._attr_supported_features |= ( + ClimateEntityFeature.TARGET_TEMPERATURE) + self._prop_target_temp = prop + elif prop.name == 'temperature': + self._prop_env_temp = prop + elif prop.name == 'relative-humidity': + self._prop_env_humi = prop + + # hvac modes + self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] + + async def async_turn_on(self) -> None: + """Turn the entity on.""" + await self.set_property_async(prop=self._prop_on, value=True) + + async def async_turn_off(self) -> None: + """Turn the entity off.""" + await self.set_property_async(prop=self._prop_on, value=False) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + await self.set_property_async( + prop=self._prop_on, value=False + if hvac_mode == HVACMode.OFF else True) + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + if ATTR_TEMPERATURE in kwargs: + temp = kwargs[ATTR_TEMPERATURE] + if temp > self.max_temp: + temp = self.max_temp + elif temp < self.min_temp: + temp = self.min_temp + + await self.set_property_async( + prop=self._prop_target_temp, value=temp) + + @property + def target_temperature(self) -> Optional[float]: + """Return the target temperature.""" + return self.get_prop_value( + prop=self._prop_target_temp) if self._prop_target_temp else None + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + return self.get_prop_value( + prop=self._prop_env_temp) if self._prop_env_temp else None + + @property + def current_humidity(self) -> Optional[int]: + """Return the current humidity.""" + return self.get_prop_value( + prop=self._prop_env_humi) if self._prop_env_humi else None + + @property + def hvac_mode(self) -> Optional[HVACMode]: + """Return the hvac mode.""" + return ( + HVACMode.HEAT if self.get_prop_value(prop=self._prop_on) + else HVACMode.OFF) diff --git a/custom_components/xiaomi_home/miot/specs/specv2entity.py b/custom_components/xiaomi_home/miot/specs/specv2entity.py index 5c64083..084cb1a 100644 --- a/custom_components/xiaomi_home/miot/specs/specv2entity.py +++ b/custom_components/xiaomi_home/miot/specs/specv2entity.py @@ -208,9 +208,32 @@ } } }, - 'entity': 'climate' + 'entity': 'air-conditioner' }, - 'air-condition-outlet': 'air-conditioner' + 'air-condition-outlet': 'air-conditioner', + 'heater': { + 'required': { + 'heater': { + 'required': { + 'properties': { + 'on': {'read', 'write'} + } + }, + 'optional': { + 'properties': {'target-temperature'} + }, + } + }, + 'optional': { + 'environment': { + 'required': {}, + 'optional': { + 'properties': {'temperature', 'relative-humidity'} + } + }, + }, + 'entity': 'heater' + } } """SPEC_SERVICE_TRANS_MAP From 7a50778d2b8c5e3c0aad417ef2ddff25fc32cce9 Mon Sep 17 00:00:00 2001 From: Hudson Brendon Date: Wed, 18 Dec 2024 00:24:46 -0300 Subject: [PATCH 10/44] Create pt.json --- .../xiaomi_home/translations/pt.json | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 custom_components/xiaomi_home/translations/pt.json diff --git a/custom_components/xiaomi_home/translations/pt.json b/custom_components/xiaomi_home/translations/pt.json new file mode 100644 index 0000000..ca72df4 --- /dev/null +++ b/custom_components/xiaomi_home/translations/pt.json @@ -0,0 +1,144 @@ +{ + "config": { + "flow_title": "Integração Xiaomi Home", + "step": { + "eula": { + "title": "Aviso de Risco", + "description": "1. As informações da sua conta Xiaomi e dos seus dispositivos serão armazenadas no sistema do Home Assistant. **A Xiaomi não pode garantir a segurança do mecanismo de armazenamento do Home Assistant**. É da sua responsabilidade impedir que a sua informação seja roubada.\r\n2. Esta integração é mantida pela comunidade open-source. Podem ocorrer problemas de estabilidade ou outros. Ao encontrar problemas ou falhas nesta integração, **deverá procurar ajuda junto da comunidade open-source, em vez de contactar o apoio ao cliente da Xiaomi**.\r\n3. Necessitará de algumas competências técnicas para manter o seu ambiente de operação local. Esta integração não é intuitiva para utilizadores iniciantes.\r\n4. Leia o ficheiro README antes de começar.\n\n5. Para garantir uma utilização estável da integração e prevenir uso indevido, **esta integração só pode ser utilizada no Home Assistant. Para mais detalhes, consulte a LICENSE**.", + "data": { + "eula": "Estou ciente dos riscos acima e disposto(a) a assumi-los voluntariamente ao utilizar a integração." + } + }, + "auth_config": { + "title": "Configuração Básica", + "description": "### Região de Login\r\nSelecione a região da sua conta Xiaomi. Pode encontrá-la na aplicação Xiaomi Home > Perfil (menu inferior) > Configurações adicionais > Sobre o Xiaomi Home.\r\n### Idioma\r\nSelecione o idioma para os nomes de dispositivos e entidades. Algumas frases sem tradução serão apresentadas em inglês.\r\n### URL de Redirecionamento OAuth2\r\nO endereço de redirecionamento para a autenticação OAuth2 é **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. O Home Assistant deve estar na mesma rede local que o terminal atual (por exemplo, o computador pessoal) e esse terminal deve conseguir aceder à página inicial do Home Assistant através deste endereço. Caso contrário, a autenticação de login pode falhar.\r\n### Notas\r\n- Para utilizadores com centenas (ou mais) de dispositivos Mi Home, a adição inicial da integração demorará algum tempo. Seja paciente.\r\n- Se o Home Assistant estiver a ser executado num ambiente Docker, assegure-se de que o modo de rede do Docker está configurado como host; caso contrário, a funcionalidade de controlo local pode não funcionar corretamente.\r\n- A funcionalidade de controlo local da integração tem algumas dependências. Leia cuidadosamente o README.", + "data": { + "cloud_server": "Região de Login", + "integration_language": "Idioma", + "oauth_redirect_url": "URL de Redirecionamento OAuth2" + } + }, + "oauth_error": { + "title": "Erro de Login", + "description": "Clique em SEGUINTE para tentar novamente." + }, + "devices_filter": { + "title": "Selecione a Casa e os Dispositivos", + "description": "## Instruções de Utilização\r\n### Modo de Controlo\r\n- Automático: Quando existir um gateway central Xiaomi disponível na rede local, o Home Assistant dará prioridade ao envio de comandos de controlo através do gateway central, permitindo um controlo local. Caso não exista um gateway central na rede local, tentará enviar comandos através da funcionalidade de controlo LAN. Apenas se estas condições não forem cumpridas, os comandos serão enviados pela nuvem.\r\n- Nuvem: Todos os comandos de controlo são enviados através da nuvem.\r\n### Importar dispositivos da casa\r\nA integração adicionará dispositivos das casas selecionadas.\n### Modo de sincronização do nome da divisão\r\nAo importar dispositivos da aplicação Xiaomi Home para o Home Assistant, a nomeação da área onde o dispositivo é adicionado segue as regras abaixo. Note que o processo de sincronização dos dispositivos não altera as definições de casa ou divisão na aplicação Xiaomi Home.\r\n- Não sincronizar: O dispositivo não será atribuído a qualquer área.\r\n- Outras opções: O dispositivo será adicionado a uma área cujo nome corresponde ao da casa e/ou divisão definida na aplicação Xiaomi Home.\r\n### Modo de depuração de ação\r\nPara as ações definidas no MIoT-Spec-V2 do dispositivo, será criada uma entidade do tipo texto juntamente com uma entidade de notificação, nas quais poderá enviar comandos de controlo ao dispositivo para fins de depuração.\r\n### Ocultar entidades não padrão\r\nOculta as entidades geradas a partir de instâncias não padrão do MIoT-Spec-V2, cujos nomes começam por \"*\".\r\n\r\n \r\n### Olá {nick_name}, selecione o modo de controlo da integração e a casa na qual deseja importar os dispositivos.", + "data": { + "ctrl_mode": "Modo de Controlo", + "home_infos": "Importar dispositivos da casa", + "area_name_rule": "Modo de sincronização do nome da divisão", + "action_debug": "Modo de depuração de ação", + "hide_non_standard_entities": "Ocultar entidades não padrão" + } + } + }, + "progress": { + "oauth": "### {link_left}Clique aqui para iniciar sessão{link_right}\r\n(Será automaticamente redirecionado após um login bem-sucedido)" + }, + "error": { + "eula_not_agree": "Por favor, leia o aviso de risco.", + "get_token_error": "Não foi possível obter a informação de autorização de login (token OAuth).", + "get_homeinfo_error": "Não foi possível obter a informação da casa.", + "mdns_discovery_error": "Exceção no serviço de descoberta de dispositivos locais.", + "get_cert_error": "Não foi possível obter o certificado do gateway central.", + "no_family_selected": "Nenhuma casa selecionada.", + "no_devices": "A casa selecionada não possui quaisquer dispositivos. Por favor, selecione uma casa que contenha dispositivos e continue.", + "no_central_device": "O [Modo Gateway Central] requer um gateway central Xiaomi disponível na rede local onde o Home Assistant está. Verifique se a casa selecionada cumpre este requisito." + }, + "abort": { + "network_connect_error": "A configuração falhou. A ligação de rede é anormal. Verifique a configuração de rede do equipamento.", + "already_configured": "A configuração para este utilizador já foi concluída. Vá à página de integrações e clique em CONFIGURAR para efetuar alterações.", + "invalid_auth_info": "A informação de autenticação expirou. Vá à página de integrações e clique em CONFIGURAR para reautenticar.", + "config_flow_error": "Erro na configuração da integração: {error}." + } + }, + "options": { + "step": { + "auth_config": { + "title": "Configuração de Autenticação", + "description": "A informação de autenticação local expirou. Por favor, reinicie o processo de autenticação.\r\n### Região de Login Atual: {cloud_server}\r\n### URL de Redirecionamento OAuth2\r\nO endereço de redirecionamento para a autenticação OAuth2 é **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. O Home Assistant deve estar na mesma rede local que o terminal atual (por exemplo, o computador) e esse terminal deve conseguir aceder à página inicial do Home Assistant através deste endereço. Caso contrário, a autenticação poderá falhar.", + "data": { + "oauth_redirect_url": "URL de Redirecionamento OAuth2" + } + }, + "oauth_error": { + "title": "Ocorreu um erro durante o login.", + "description": "Clique em SEGUINTE para tentar novamente." + }, + "config_options": { + "title": "Opções de Configuração", + "description": "### Olá, {nick_name}\r\n\r\nID Xiaomi: {uid}\r\nRegião de Login Atual: {cloud_server}\r\n\r\nSelecione as opções que pretende configurar e depois clique em SEGUINTE.", + "data": { + "integration_language": "Idioma da Integração", + "update_user_info": "Atualizar informação do utilizador", + "update_devices": "Atualizar lista de dispositivos", + "action_debug": "Modo de depuração de ação", + "hide_non_standard_entities": "Ocultar entidades não padrão", + "update_trans_rules": "Atualizar regras de conversão de entidades", + "update_lan_ctrl_config": "Atualizar configuração de controlo LAN" + } + }, + "update_user_info": { + "title": "Atualizar Alcunha do Utilizador", + "description": "Olá {nick_name}, pode modificar a sua alcunha personalizada abaixo.", + "data": { + "nick_name": "Alcunha" + } + }, + "devices_filter": { + "title": "Selecionar novamente a Casa e os Dispositivos", + "description": "## Instruções de Utilização\r\n### Modo de Controlo\r\n- Automático: Quando houver um gateway central Xiaomi disponível na rede local, o Home Assistant priorizará o envio de comandos através dele para obter controlo local. Se não existir um gateway central, tentará enviar comandos através da função de controlo LAN da Xiaomi. Apenas se estas condições não forem satisfeitas, os comandos serão enviados pela nuvem.\r\n- Nuvem: Todos os comandos de controlo são enviados através da nuvem.\r\n### Importar dispositivos da casa\r\nA integração adicionará dispositivos das casas selecionadas.\r\n \r\n### Olá {nick_name}, selecione o modo de controlo da integração e a casa da qual pretende importar dispositivos.", + "data": { + "ctrl_mode": "Modo de Controlo", + "home_infos": "Importar dispositivos da casa" + } + }, + "update_trans_rules": { + "title": "Atualizar Regras de Transformação de Entidades", + "description": "## Instruções de Utilização\r\n- Atualiza a informação das entidades dos dispositivos na instância atual da integração, incluindo configuração multilingue MIoT-Spec-V2, tradução de booleanos e filtragem de modelos.\r\n- **Aviso**: Esta é uma configuração global e atualizará a cache local, afetando todas as instâncias da integração.\r\n- Esta operação levará algum tempo, seja paciente. Selecione \"Confirmar a atualização\" e clique em \"SEGUINTE\" para iniciar a atualização de **{urn_count}** regras, caso contrário, ignore esta etapa.\r\n", + "data": { + "confirm": "Confirmar a atualização" + } + }, + "update_lan_ctrl_config": { + "title": "Atualizar Configuração de Controlo LAN", + "description": "## Instruções de Utilização\r\nAtualize as configurações para a funcionalidade de controlo LAN da Xiaomi. Quando a nuvem e o gateway central não puderem controlar os dispositivos, a integração tentará controlá-los através da LAN. Se não selecionar nenhuma interface de rede, o controlo LAN não terá efeito.\r\n- Apenas dispositivos compatíveis com MIoT-Spec-V2 ligados via IP na LAN são suportados. Alguns dispositivos produzidos antes de 2020 podem não suportar controlo LAN ou subscrição LAN.\r\n- Selecione a(s) placa(s) de rede que estejam na mesma rede que os dispositivos a controlar. Pode selecionar várias placas. Se o Home Assistant tiver duas ou mais ligações à rede local devido à seleção de várias placas, é recomendado selecionar a que tiver melhor ligação, caso contrário poderá afetar negativamente o desempenho dos dispositivos.\r\n- Se houver dispositivos terminais (colunas Xiaomi com ecrã, telemóveis, etc.) na LAN que suportem controlo local, a ativação da subscrição LAN pode causar anomalias em automações e dispositivos locais.\r\n- **Aviso**: Esta é uma configuração global, afetando todas as instâncias da integração. Utilize com cautela.\r\n{notice_net_dup}", + "data": { + "net_interfaces": "Selecione a(s) interface(s) de rede a utilizar", + "enable_subscribe": "Ativar subscrição LAN" + } + }, + "config_confirm": { + "title": "Confirmar Configuração", + "description": "Olá **{nick_name}**, confirme a informação da configuração mais recente e depois clique em SUBMETER.\r\nA integração será recarregada com a configuração atualizada.\r\n\r\nIdioma da Integração:\t{lang_new}\r\nAlcunha:\t{nick_name_new}\r\nModo de depuração de ação:\t{action_debug}\r\nOcultar entidades não padrão:\t{hide_non_standard_entities}\r\nAlterações aos Dispositivos:\tAdicionar **{devices_add}** dispositivos, Remover **{devices_remove}** dispositivos\r\nAlteração das Regras de Transformação:\tExistem **{trans_rules_count}** regras no total, com **{trans_rules_count_success}** regras atualizadas", + "data": { + "confirm": "Confirmar a alteração" + } + } + }, + "progress": { + "oauth": "### {link_left}Por favor, clique aqui para voltar a iniciar sessão{link_right}" + }, + "error": { + "not_auth": "Não autenticado. Por favor, clique no link de autenticação para confirmar a sua identidade.", + "get_token_error": "Não foi possível obter a informação de autorização de login (token OAuth).", + "get_homeinfo_error": "Não foi possível obter a informação da casa.", + "get_cert_error": "Não foi possível obter o certificado do gateway central.", + "no_devices": "A casa selecionada não possui quaisquer dispositivos. Por favor, selecione uma casa com dispositivos e continue.", + "no_family_selected": "Nenhuma casa selecionada.", + "no_central_device": "O [Modo Gateway Central] requer um gateway central Xiaomi disponível na rede local onde o Home Assistant está. Verifique se a casa selecionada cumpre este requisito.", + "mdns_discovery_error": "Exceção no serviço de descoberta de dispositivos locais.", + "update_config_error": "Não foi possível atualizar a informação de configuração.", + "not_confirm": "As alterações não foram confirmadas. Por favor, confirme a alteração antes de submeter." + }, + "abort": { + "network_connect_error": "A configuração falhou. A ligação de rede é anormal. Verifique a configuração da rede do equipamento.", + "options_flow_error": "Erro na reconfiguração da integração: {error}", + "re_add": "Por favor, volte a adicionar a integração. Mensagem de erro: {error}", + "storage_error": "Exceção no módulo de armazenamento da integração. Tente novamente ou volte a adicionar a integração: {error}", + "inconsistent_account": "A informação da conta é inconsistente. Por favor, inicie sessão com a conta correta." + } + } +} From 83dbceac6de545be57fc35f76d7f5c44f28ee7c8 Mon Sep 17 00:00:00 2001 From: Hudson Brendon Date: Wed, 18 Dec 2024 00:16:15 -0300 Subject: [PATCH 11/44] Create pt-BR.json --- .../xiaomi_home/translations/pt-BR.json | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 custom_components/xiaomi_home/translations/pt-BR.json diff --git a/custom_components/xiaomi_home/translations/pt-BR.json b/custom_components/xiaomi_home/translations/pt-BR.json new file mode 100644 index 0000000..4ae8c2e --- /dev/null +++ b/custom_components/xiaomi_home/translations/pt-BR.json @@ -0,0 +1,144 @@ +{ + "config": { + "flow_title": "Integração Xiaomi Home", + "step": { + "eula": { + "title": "Aviso de risco", + "description": "1. Suas informações de usuário Xiaomi e informações dos dispositivos serão armazenadas no sistema Home Assistant. **A Xiaomi não pode garantir a segurança do mecanismo de armazenamento do Home Assistant**. Você é responsável por evitar que suas informações sejam roubadas.\r\n2. Esta integração é mantida pela comunidade open-source. Podem haver problemas de estabilidade ou outros problemas. Ao encontrar falhas ou erros nesta integração, **você deve buscar ajuda da comunidade open-source em vez de contatar o suporte da Xiaomi**.\r\n3. Você precisará de certa habilidade técnica para manter seu ambiente operacional local. Esta integração não é amigável para iniciantes.\r\n4. Por favor, leia o arquivo README antes de começar.\n\n5. Para garantir o uso estável da integração e evitar abusos da interface, **esta integração só é permitida para uso no Home Assistant. Para mais detalhes, consulte a LICENSE**.", + "data": { + "eula": "Estou ciente dos riscos acima e disposto(a) a assumi-los voluntariamente ao utilizar a integração." + } + }, + "auth_config": { + "title": "Configuração básica", + "description": "### Região de Login\r\nSelecione a região da sua conta Xiaomi. Você pode encontrá-la no aplicativo Xiaomi Home > Perfil (localizado no menu inferior) > Configurações adicionais > Sobre o Xiaomi Home.\r\n### Idioma\r\nSelecione o idioma dos nomes dos dispositivos e entidades. Algumas frases sem tradução serão exibidas em inglês.\r\n### URL de Redirecionamento OAuth2\r\nO endereço de redirecionamento da autenticação OAuth2 é **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. O Home Assistant precisa estar na mesma rede local que o terminal atual (por exemplo, o computador pessoal) e o terminal precisa acessar a página inicial do Home Assistant através desse endereço. Caso contrário, a autenticação de login pode falhar.\r\n### Observações\r\n- Para usuários com centenas ou mais dispositivos Mi Home, a adição inicial da integração levará algum tempo. Seja paciente.\r\n- Se o Home Assistant estiver sendo executado em um ambiente Docker, certifique-se de que o modo de rede do Docker esteja definido como host, caso contrário a funcionalidade de controle local pode não funcionar corretamente.\r\n- A funcionalidade de controle local da integração tem algumas dependências. Por favor, leia o README atentamente.", + "data": { + "cloud_server": "Região de Login", + "integration_language": "Idioma", + "oauth_redirect_url": "URL de Redirecionamento OAuth2" + } + }, + "oauth_error": { + "title": "Erro de Login", + "description": "Clique em AVANÇAR para tentar novamente." + }, + "devices_filter": { + "title": "Selecione a Casa e os Dispositivos", + "description": "## Instruções de Uso\r\n### Modo de controle\r\n- Auto: Quando houver um gateway central Xiaomi disponível na rede local, o Home Assistant priorizará o envio de comandos de controle do dispositivo através do gateway central, obtendo assim controle local. Se não houver gateway central na rede local, ele tentará enviar comandos através da função de controle LAN da Xiaomi. Somente quando as condições de controle local acima não forem atendidas, os comandos serão enviados pela nuvem.\r\n- Nuvem: Todos os comandos de controle são enviados através da nuvem.\r\n### Importar dispositivos da casa\r\nA integração adicionará dispositivos das casas selecionadas.\n### Modo de sincronização do nome do cômodo\r\nAo importar dispositivos do aplicativo Xiaomi Home para o Home Assistant, a convenção de nomeação da área onde o dispositivo é adicionado é a seguinte. Observe que o processo de sincronização do dispositivo não altera as configurações de casa ou cômodo no aplicativo Xiaomi Home.\r\n- Não sincronizar: O dispositivo não será adicionado a nenhuma área.\r\n- Outras opções: O dispositivo será adicionado a uma área nomeada de acordo com o nome da casa e/ou do cômodo que já existem no aplicativo Xiaomi Home.\r\n### Modo de depuração para ação\r\nPara as ações definidas no MIoT-Spec-V2 do dispositivo, será criada uma entidade de texto juntamente com uma entidade de notificação, nas quais você poderá enviar comandos de controle ao dispositivo para fins de depuração.\r\n### Ocultar entidades criadas não padrão\r\nOculta as entidades geradas a partir de instâncias não padrão do MIoT-Spec-V2, cujos nomes começam com \"*\".\r\n\r\n \r\n### Olá {nick_name}, selecione o modo de controle da integração e a casa onde estão os dispositivos que você deseja importar.", + "data": { + "ctrl_mode": "Modo de controle", + "home_infos": "Importar dispositivos da casa", + "area_name_rule": "Modo de sincronização do nome do cômodo", + "action_debug": "Modo de depuração para ação", + "hide_non_standard_entities": "Ocultar entidades não padrão criadas" + } + } + }, + "progress": { + "oauth": "### {link_left}Clique aqui para fazer login{link_right}\r\n(Você será redirecionado automaticamente para a próxima página após um login bem-sucedido)" + }, + "error": { + "eula_not_agree": "Por favor, leia o aviso de risco.", + "get_token_error": "Falha ao obter as informações de autorização de login (token OAuth).", + "get_homeinfo_error": "Falha ao obter as informações da casa.", + "mdns_discovery_error": "Exceção no serviço de descoberta de dispositivos locais.", + "get_cert_error": "Falha ao obter o certificado do gateway central.", + "no_family_selected": "Nenhuma casa selecionada.", + "no_devices": "A casa selecionada não possui nenhum dispositivo. Por favor, escolha uma casa que contenha dispositivos e continue.", + "no_central_device": "[Modo Gateway Central] requer um gateway central Xiaomi disponível na rede local onde o Home Assistant está. Verifique se a casa selecionada atende a esse requisito." + }, + "abort": { + "network_connect_error": "Configuração falhou. A conexão de rede está anormal. Verifique a configuração de rede do equipamento.", + "already_configured": "A configuração para este usuário já foi concluída. Vá para a página de integrações e clique no botão CONFIGURAR para modificações.", + "invalid_auth_info": "As informações de autenticação expiraram. Vá para a página de integrações e clique em CONFIGURAR para reautenticar.", + "config_flow_error": "Erro na configuração da integração: {error}." + } + }, + "options": { + "step": { + "auth_config": { + "title": "Configuração de Autenticação", + "description": "As informações de autenticação local expiraram. Por favor, reinicie o processo de autenticação.\r\n### Região de Login Atual: {cloud_server}\r\n### URL de Redirecionamento OAuth2\r\nO endereço de redirecionamento da autenticação OAuth2 é **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. O Home Assistant precisa estar na mesma rede local que o terminal atual (por exemplo, o computador pessoal) e o terminal precisa acessar a página inicial do Home Assistant através desse endereço. Caso contrário, a autenticação de login pode falhar.", + "data": { + "oauth_redirect_url": "URL de Redirecionamento OAuth2" + } + }, + "oauth_error": { + "title": "Ocorreu um erro durante o login.", + "description": "Clique em AVANÇAR para tentar novamente." + }, + "config_options": { + "title": "Opções de Configuração", + "description": "### Olá, {nick_name}\r\n\r\nID Xiaomi: {uid}\r\nRegião de Login Atual: {cloud_server}\r\n\r\nSelecione as opções que você deseja configurar e clique em AVANÇAR.", + "data": { + "integration_language": "Idioma da Integração", + "update_user_info": "Atualizar informações do usuário", + "update_devices": "Atualizar lista de dispositivos", + "action_debug": "Modo de depuração para ação", + "hide_non_standard_entities": "Ocultar entidades não padrão criadas", + "update_trans_rules": "Atualizar regras de conversão de entidades", + "update_lan_ctrl_config": "Atualizar configuração de controle LAN" + } + }, + "update_user_info": { + "title": "Atualizar Apelido do Usuário", + "description": "Olá {nick_name}, você pode modificar seu apelido personalizado abaixo.", + "data": { + "nick_name": "Apelido" + } + }, + "devices_filter": { + "title": "Selecionar novamente Casa e Dispositivos", + "description": "## Instruções de Uso\r\n### Modo de controle\r\n- Auto: Quando houver um gateway central Xiaomi disponível na rede local, o Home Assistant priorizará o envio de comandos através dele para obter controle local. Caso não haja, tentará enviar comandos através da função de controle LAN da Xiaomi. Somente se as condições anteriores não forem atendidas, o controle será feito pela nuvem.\r\n- Nuvem: Todos os comandos de controle são enviados pela nuvem.\r\n### Importar dispositivos da casa\r\nA integração adicionará dispositivos das casas selecionadas.\r\n \r\n### Olá {nick_name}, selecione o modo de controle da integração e a casa de onde deseja importar dispositivos.", + "data": { + "ctrl_mode": "Modo de controle", + "home_infos": "Importar dispositivos da casa" + } + }, + "update_trans_rules": { + "title": "Atualizar Regras de Transformação de Entidades", + "description": "## Instruções de Uso\r\n- Atualiza as informações das entidades dos dispositivos na instância atual da integração, incluindo configuração multilíngue MIoT-Spec-V2, tradução de booleanos e filtragem de modelos.\r\n- **Aviso**: Esta é uma configuração global e atualizará o cache local. Ela afetará todas as instâncias da integração.\r\n- Esta operação levará algum tempo, seja paciente. Marque \"Confirmar atualização\" e clique em \"Avançar\" para iniciar a atualização de **{urn_count}** regras, caso contrário, pule.\r\n", + "data": { + "confirm": "Confirmar a atualização" + } + }, + "update_lan_ctrl_config": { + "title": "Atualizar configuração de controle LAN", + "description": "## Instruções de Uso\r\nAtualize as configurações para a função de controle LAN da Xiaomi. Quando a nuvem e o gateway central não puderem controlar os dispositivos, a integração tentará controlá-los através da LAN. Se nenhuma placa de rede for selecionada, o controle LAN não terá efeito.\r\n- Somente dispositivos compatíveis com MIoT-Spec-V2 conectados via IP na LAN são suportados. Alguns dispositivos produzidos antes de 2020 podem não suportar controle LAN ou assinatura LAN.\r\n- Selecione a(s) placa(s) de rede que estão na mesma rede que os dispositivos a serem controlados. É possível selecionar várias placas. Se o Home Assistant tiver duas ou mais conexões com a rede local devido a múltiplas placas, recomenda-se selecionar a que tiver melhor conexão de rede. Caso contrário, isso pode afetar o desempenho.\r\n- Se houver dispositivos terminais (alto-falantes Xiaomi com tela, celular, etc.) na LAN que suportem controle local, habilitar a assinatura LAN pode causar comportamentos anormais em automações e dispositivos locais.\r\n- **Aviso**: Esta é uma configuração global. Afetará todas as instâncias da integração. Use com cautela.\r\n{notice_net_dup}", + "data": { + "net_interfaces": "Selecione a placa de rede a ser usada", + "enable_subscribe": "Habilitar assinatura LAN" + } + }, + "config_confirm": { + "title": "Confirmar Configuração", + "description": "Olá **{nick_name}**, confirme as informações da configuração mais recente e depois clique em ENVIAR.\r\nA integração será recarregada com a configuração atualizada.\r\n\r\nIdioma da Integração:\t{lang_new}\r\nApelido:\t{nick_name_new}\r\nModo de depuração para ação:\t{action_debug}\r\nOcultar entidades não padrão criadas:\t{hide_non_standard_entities}\r\nAlterações de Dispositivos:\tAdicionar **{devices_add}** dispositivos, Remover **{devices_remove}** dispositivos\r\nAlteração nas Regras de Transformação:\tUm total de **{trans_rules_count}** regras, e **{trans_rules_count_success}** regras atualizadas", + "data": { + "confirm": "Confirmar a mudança" + } + } + }, + "progress": { + "oauth": "### {link_left}Por favor, clique aqui para relogar{link_right}" + }, + "error": { + "not_auth": "Não autenticado. Por favor, clique no link de autenticação para autenticar sua identidade.", + "get_token_error": "Falha ao obter as informações de autorização de login (token OAuth).", + "get_homeinfo_error": "Falha ao obter as informações da casa.", + "get_cert_error": "Falha ao obter o certificado do gateway central.", + "no_devices": "A casa selecionada não possui nenhum dispositivo. Por favor, escolha uma casa com dispositivos e continue.", + "no_family_selected": "Nenhuma casa selecionada.", + "no_central_device": "[Modo Gateway Central] requer um gateway central Xiaomi disponível na rede local onde o Home Assistant está. Verifique se a casa selecionada atende a esse requisito.", + "mdns_discovery_error": "Exceção no serviço de descoberta de dispositivos locais.", + "update_config_error": "Falha ao atualizar as informações de configuração.", + "not_confirm": "As alterações não foram confirmadas. Por favor, confirme a mudança antes de enviar." + }, + "abort": { + "network_connect_error": "Configuração falhou. A conexão de rede está anormal. Verifique a configuração da rede do equipamento.", + "options_flow_error": "Erro na reconfiguração da integração: {error}", + "re_add": "Por favor, adicione novamente a integração. Mensagem de erro: {error}", + "storage_error": "Exceção no módulo de armazenamento da integração. Tente novamente ou readicione a integração: {error}", + "inconsistent_account": "As informações da conta são inconsistentes. Por favor, faça login com a conta correta." + } + } +} From 6bb4bf32d76f0f97a1a8ea4d97a1b3809393742a Mon Sep 17 00:00:00 2001 From: KNOOP Date: Tue, 17 Dec 2024 16:20:02 +0800 Subject: [PATCH 12/44] fix: handle UnitOfConductivity import Move unit imports inside function and add fallback for older versions. Resolves #123 --- .../xiaomi_home/miot/miot_device.py | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/custom_components/xiaomi_home/miot/miot_device.py b/custom_components/xiaomi_home/miot/miot_device.py index 56637d6..3531ebf 100644 --- a/custom_components/xiaomi_home/miot/miot_device.py +++ b/custom_components/xiaomi_home/miot/miot_device.py @@ -59,20 +59,6 @@ LIGHT_LUX, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS, - UnitOfEnergy, - UnitOfElectricCurrent, - UnitOfElectricPotential, - UnitOfInformation, - UnitOfLength, - UnitOfMass, - UnitOfSpeed, - UnitOfTime, - UnitOfTemperature, - UnitOfPressure, - UnitOfPower, - UnitOfVolume, - UnitOfVolumeFlowRate, - UnitOfConductivity ) from homeassistant.helpers.entity import DeviceInfo from homeassistant.components.switch import SwitchDeviceClass @@ -505,7 +491,8 @@ def parse_miot_property_entity( prop_access.add('read') if prop.writable: prop_access.add('write') - if prop_access != (SPEC_PROP_TRANS_MAP['entities'][platform]['access']): + if prop_access != (SPEC_PROP_TRANS_MAP[ + 'entities'][platform]['access']): return None if prop.format_ not in SPEC_PROP_TRANS_MAP[ 'entities'][platform]['format']: @@ -584,7 +571,24 @@ def spec_transform(self) -> None: self.append_action(action=action) def unit_convert(self, spec_unit: str) -> Optional[str]: - return { + """Convert MIoT unit to Home Assistant unit.""" + from homeassistant.const import ( + UnitOfEnergy, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfInformation, + UnitOfLength, + UnitOfMass, + UnitOfSpeed, + UnitOfTime, + UnitOfTemperature, + UnitOfPressure, + UnitOfPower, + UnitOfVolume, + UnitOfVolumeFlowRate, + ) + + unit_map = { 'percentage': PERCENTAGE, 'weeks': UnitOfTime.WEEKS, 'days': UnitOfTime.DAYS, @@ -616,11 +620,18 @@ def unit_convert(self, spec_unit: str) -> Optional[str]: 'm': UnitOfLength.METERS, 'km': UnitOfLength.KILOMETERS, 'm3/h': UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, - 'μS/cm': UnitOfConductivity.MICROSIEMENS_PER_CM, 'gram': UnitOfMass.GRAMS, 'dB': SIGNAL_STRENGTH_DECIBELS, 'kB': UnitOfInformation.KILOBYTES, - }.get(spec_unit, None) + } + + try: + from homeassistant.const import UnitOfConductivity + unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM + except ImportError: + unit_map['μS/cm'] = 'μS/cm' + + return unit_map.get(spec_unit, None) def icon_convert(self, spec_unit: str) -> Optional[str]: if spec_unit in ['percentage']: @@ -1170,8 +1181,8 @@ async def async_added_to_hass(self) -> None: handler=self.__on_device_state_changed) # Sub value changed 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( From c744919032ca7ae81311ebae91b1d920db7752c6 Mon Sep 17 00:00:00 2001 From: KNOOP Date: Tue, 17 Dec 2024 18:21:03 +0800 Subject: [PATCH 13/44] fix: handle UnitOfConductivity import #54 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move unit imports inside function 和 add fallback for older versions. Resolves --- .../xiaomi_home/miot/miot_device.py | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/custom_components/xiaomi_home/miot/miot_device.py b/custom_components/xiaomi_home/miot/miot_device.py index 3531ebf..8de4c1c 100644 --- a/custom_components/xiaomi_home/miot/miot_device.py +++ b/custom_components/xiaomi_home/miot/miot_device.py @@ -54,11 +54,24 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, LIGHT_LUX, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS, + UnitOfEnergy, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfInformation, + UnitOfLength, + UnitOfMass, + UnitOfSpeed, + UnitOfTime, + UnitOfTemperature, + UnitOfPressure, + UnitOfPower, + UnitOfVolume, + UnitOfVolumeFlowRate, ) from homeassistant.helpers.entity import DeviceInfo from homeassistant.components.switch import SwitchDeviceClass @@ -572,22 +585,6 @@ def spec_transform(self) -> None: def unit_convert(self, spec_unit: str) -> Optional[str]: """Convert MIoT unit to Home Assistant unit.""" - from homeassistant.const import ( - UnitOfEnergy, - UnitOfElectricCurrent, - UnitOfElectricPotential, - UnitOfInformation, - UnitOfLength, - UnitOfMass, - UnitOfSpeed, - UnitOfTime, - UnitOfTemperature, - UnitOfPressure, - UnitOfPower, - UnitOfVolume, - UnitOfVolumeFlowRate, - ) - unit_map = { 'percentage': PERCENTAGE, 'weeks': UnitOfTime.WEEKS, @@ -625,6 +622,7 @@ def unit_convert(self, spec_unit: str) -> Optional[str]: 'kB': UnitOfInformation.KILOBYTES, } + # Handle UnitOfConductivity separately since it might not be available in all HA versions try: from homeassistant.const import UnitOfConductivity unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM From b498a708acf860f0a3be0f0794318aa055d8027a Mon Sep 17 00:00:00 2001 From: Guoliang Li Date: Wed, 18 Dec 2024 15:44:04 +0800 Subject: [PATCH 14/44] style: ignore pylint warning --- custom_components/xiaomi_home/miot/miot_device.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/xiaomi_home/miot/miot_device.py b/custom_components/xiaomi_home/miot/miot_device.py index 8de4c1c..8fdc597 100644 --- a/custom_components/xiaomi_home/miot/miot_device.py +++ b/custom_components/xiaomi_home/miot/miot_device.py @@ -624,6 +624,7 @@ def unit_convert(self, spec_unit: str) -> Optional[str]: # Handle UnitOfConductivity separately since it might not be available in all HA versions try: + # pylint: disable=import-outside-toplevel from homeassistant.const import UnitOfConductivity unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM except ImportError: From e70acd8421a5105e7cf12b2ead6bfb8269a0ff9c Mon Sep 17 00:00:00 2001 From: Guoliang Li Date: Wed, 18 Dec 2024 15:48:51 +0800 Subject: [PATCH 15/44] style: update comments to meet coding standards --- custom_components/xiaomi_home/miot/miot_device.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/xiaomi_home/miot/miot_device.py b/custom_components/xiaomi_home/miot/miot_device.py index 8fdc597..3cda670 100644 --- a/custom_components/xiaomi_home/miot/miot_device.py +++ b/custom_components/xiaomi_home/miot/miot_device.py @@ -622,7 +622,8 @@ def unit_convert(self, spec_unit: str) -> Optional[str]: 'kB': UnitOfInformation.KILOBYTES, } - # Handle UnitOfConductivity separately since it might not be available in all HA versions + # Handle UnitOfConductivity separately since + # it might not be available in all HA versions try: # pylint: disable=import-outside-toplevel from homeassistant.const import UnitOfConductivity From ce4f23b1bd0a695d8b212d494e4313fe6744f2bf Mon Sep 17 00:00:00 2001 From: Guoliang Li Date: Wed, 18 Dec 2024 15:54:37 +0800 Subject: [PATCH 16/44] style: fix the trailing space problem reported by pylint --- custom_components/xiaomi_home/miot/miot_device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/xiaomi_home/miot/miot_device.py b/custom_components/xiaomi_home/miot/miot_device.py index 3cda670..fa48345 100644 --- a/custom_components/xiaomi_home/miot/miot_device.py +++ b/custom_components/xiaomi_home/miot/miot_device.py @@ -622,7 +622,7 @@ def unit_convert(self, spec_unit: str) -> Optional[str]: 'kB': UnitOfInformation.KILOBYTES, } - # Handle UnitOfConductivity separately since + # Handle UnitOfConductivity separately since # it might not be available in all HA versions try: # pylint: disable=import-outside-toplevel From 6e2de896c39731844a9cc04847ff949a62cb70d0 Mon Sep 17 00:00:00 2001 From: topsworld Date: Wed, 18 Dec 2024 17:42:40 +0800 Subject: [PATCH 17/44] feat: update xiaomi heater ctrl logic --- custom_components/xiaomi_home/climate.py | 36 +++++++++++++++++++ .../xiaomi_home/miot/specs/specv2entity.py | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index ccda2b6..106abb9 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -484,10 +484,13 @@ class Heater(MIoTServiceEntity, ClimateEntity): _prop_on: Optional[MIoTSpecProperty] _prop_mode: Optional[MIoTSpecProperty] _prop_target_temp: Optional[MIoTSpecProperty] + _prop_heat_level: Optional[MIoTSpecProperty] # service: environment _prop_env_temp: Optional[MIoTSpecProperty] _prop_env_humi: Optional[MIoTSpecProperty] + _heat_level_map: Optional[dict[int, str]] + def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: @@ -495,12 +498,15 @@ def __init__( super().__init__(miot_device=miot_device, entity_data=entity_data) self._attr_icon = 'mdi:air-conditioner' self._attr_supported_features = ClimateEntityFeature(0) + self._attr_preset_modes = [] self._prop_on = None self._prop_mode = None self._prop_target_temp = None + self._prop_heat_level = None self._prop_env_temp = None self._prop_env_humi = None + self._heat_level_map = None # properties for prop in entity_data.props: @@ -523,6 +529,21 @@ def __init__( self._attr_supported_features |= ( ClimateEntityFeature.TARGET_TEMPERATURE) self._prop_target_temp = prop + elif prop.name == 'heat-level': + if ( + not isinstance(prop.value_list, list) + or not prop.value_list + ): + _LOGGER.error( + 'invalid heat-level value_list, %s', self.entity_id) + continue + self._heat_level_map = { + item['value']: item['description'] + for item in prop.value_list} + self._attr_preset_modes = list(self._heat_level_map.values()) + self._attr_supported_features |= ( + ClimateEntityFeature.PRESET_MODE) + self._prop_heat_level = prop elif prop.name == 'temperature': self._prop_env_temp = prop elif prop.name == 'relative-humidity': @@ -557,6 +578,13 @@ async def async_set_temperature(self, **kwargs): await self.set_property_async( prop=self._prop_target_temp, value=temp) + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode.""" + await self.set_property_async( + self._prop_heat_level, + value=self.get_map_value( + map_=self._heat_level_map, description=preset_mode)) + @property def target_temperature(self) -> Optional[float]: """Return the target temperature.""" @@ -581,3 +609,11 @@ def hvac_mode(self) -> Optional[HVACMode]: return ( HVACMode.HEAT if self.get_prop_value(prop=self._prop_on) else HVACMode.OFF) + + @property + def preset_mode(self) -> Optional[str]: + return ( + self.get_map_description( + map_=self._heat_level_map, + key=self.get_prop_value(prop=self._prop_heat_level)) + if self._prop_heat_level else None) diff --git a/custom_components/xiaomi_home/miot/specs/specv2entity.py b/custom_components/xiaomi_home/miot/specs/specv2entity.py index 084cb1a..4a57867 100644 --- a/custom_components/xiaomi_home/miot/specs/specv2entity.py +++ b/custom_components/xiaomi_home/miot/specs/specv2entity.py @@ -220,7 +220,7 @@ } }, 'optional': { - 'properties': {'target-temperature'} + 'properties': {'target-temperature', 'heat-level'} }, } }, From fec5d10f42c200c20dd427bafa49aa5a46c54f55 Mon Sep 17 00:00:00 2001 From: fanjinyu Date: Wed, 18 Dec 2024 19:32:00 +0800 Subject: [PATCH 18/44] add pt and pt-BR language support in i18n --- .../xiaomi_home/miot/i18n/pt-BR.json | 95 +++++++++++++++++++ .../xiaomi_home/miot/i18n/pt.json | 95 +++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 custom_components/xiaomi_home/miot/i18n/pt-BR.json create mode 100644 custom_components/xiaomi_home/miot/i18n/pt.json diff --git a/custom_components/xiaomi_home/miot/i18n/pt-BR.json b/custom_components/xiaomi_home/miot/i18n/pt-BR.json new file mode 100644 index 0000000..8df352b --- /dev/null +++ b/custom_components/xiaomi_home/miot/i18n/pt-BR.json @@ -0,0 +1,95 @@ +{ + "config": { + "other": { + "devices": "dispositivos", + "found_central_gateway": "encontrado o gateway central local" + }, + "control_mode": { + "auto": "automático", + "cloud": "nuvem" + }, + "room_name_rule": { + "none": "não sincronizado", + "home_room": "Nome da casa e nome do quarto (Xiaomi Home Quarto)", + "room": "Nome do quarto (Quarto)", + "home": "Nome da casa (Xiaomi Home)" + }, + "option_status": { + "enable": "habilitado", + "disable": "desabilitado" + }, + "lan_ctrl_config": { + "notice_net_dup": "\r\n**[Aviso]** Detectado múltiplas interfaces de rede que podem estar conectando à mesma rede, por favor, selecione a correta.", + "net_unavailable": "Interface indisponível" + } + }, + "miot": { + "client": { + "invalid_oauth_info": "Informações de autenticação inválidas, a conexão com a nuvem estará indisponível. Vá para a página de integração do Xiaomi Home e clique em 'Opções' para reautenticar.", + "invalid_device_cache": "Informações de dispositivo no cache inválidas. Vá para a página de integração do Xiaomi Home e clique em 'Opções -> Atualizar lista de dispositivos' para atualizar as informações locais.", + "invalid_cert_info": "Certificado de usuário inválido. A conexão local do gateway central estará indisponível. Vá para a página de integração do Xiaomi Home e clique em 'Opções' para reautenticar.", + "device_cloud_error": "Erro ao obter informações do dispositivo da nuvem. Verifique a conexão da rede local.", + "xiaomi_home_error_title": "Erro de Integração do Xiaomi Home", + "xiaomi_home_error": "Erro detectado em **{nick_name}({uid}, {cloud_server})**. Vá para a página de opções para reconfigurar.\n\n**Erro**: \n{message}", + "device_list_changed_title": "Mudança na lista de dispositivos do Xiaomi Home", + "device_list_changed": "Detectado que as informações do dispositivo **{nick_name}({uid}, {cloud_server})** mudaram. Vá para a página de integração e clique em 'Opções -> Atualizar lista de dispositivos' para atualizar as informações locais.\n\nStatus atual da rede: {network_status}\n{message}\n", + "device_list_add": "\n**{count} dispositivos novos**: \n{message}", + "device_list_del": "\n**{count} dispositivos não disponíveis**: \n{message}", + "device_list_offline": "\n**{count} dispositivos offline**: \n{message}", + "network_status_online": "online", + "network_status_offline": "offline", + "device_exec_error": "Erro na execução" + } + }, + "error": { + "common": { + "-10000": "Erro desconhecido", + "-10001": "Serviço indisponível", + "-10002": "Parâmetro inválido", + "-10003": "Recursos insuficientes", + "-10004": "Erro interno", + "-10005": "Permissões insuficientes", + "-10006": "Execução expirada", + "-10007": "Dispositivo offline ou inexistente", + "-10020": "OAuth2 não autorizado", + "-10030": "Token inválido (HTTP)", + "-10040": "Formato de mensagem inválido", + "-10050": "Certificado inválido", + "-704000000": "Erro desconhecido", + "-704010000": "Não autorizado (o dispositivo pode ter sido excluído)", + "-704014006": "Descrição do dispositivo não encontrada", + "-704030013": "Propriedade não pode ser lida", + "-704030023": "Propriedade não pode ser escrita", + "-704030033": "Propriedade não pode ser assinada", + "-704040002": "Serviço inexistente", + "-704040003": "Propriedade inexistente", + "-704040004": "Evento inexistente", + "-704040005": "Ação inexistente", + "-704040999": "Funcionalidade não lançada", + "-704042001": "Dispositivo inexistente", + "-704042011": "Dispositivo offline", + "-704053036": "Tempo de operação do dispositivo expirado", + "-704053100": "Dispositivo não pode executar esta operação no estado atual", + "-704083036": "Tempo de operação do dispositivo expirado", + "-704090001": "Dispositivo inexistente", + "-704220008": "ID inválido", + "-704220025": "Número de parâmetros de ação incompatível", + "-704220035": "Parâmetro de ação incorreto", + "-704220043": "Valor da propriedade incorreto", + "-704222034": "Valor de retorno de ação incorreto", + "-705004000": "Erro desconhecido", + "-705004501": "Erro desconhecido", + "-705201013": "Propriedade não pode ser lida", + "-705201015": "Erro na execução da ação", + "-705201023": "Propriedade não pode ser escrita", + "-705201033": "Propriedade não pode ser assinada", + "-706012000": "Erro desconhecido", + "-706012013": "Propriedade não pode ser lida", + "-706012015": "Erro na execução da ação", + "-706012023": "Propriedade não pode ser escrita", + "-706012033": "Propriedade não pode ser assinada", + "-706012043": "Valor da propriedade incorreto", + "-706014006": "Descrição do dispositivo não encontrada" + } + } +} \ No newline at end of file diff --git a/custom_components/xiaomi_home/miot/i18n/pt.json b/custom_components/xiaomi_home/miot/i18n/pt.json new file mode 100644 index 0000000..dd30774 --- /dev/null +++ b/custom_components/xiaomi_home/miot/i18n/pt.json @@ -0,0 +1,95 @@ +{ + "config": { + "other": { + "devices": "dispositivos", + "found_central_gateway": ", encontrou a central de gateway local" + }, + "control_mode": { + "auto": "Automático", + "cloud": "Nuvem" + }, + "room_name_rule": { + "none": "Não sincronizar", + "home_room": "Nome da casa e Nome do quarto (Xiaomi Home Quarto)", + "room": "Nome do quarto (Quarto)", + "home": "Nome da casa (Xiaomi Home)" + }, + "option_status": { + "enable": "Habilitar", + "disable": "Desabilitar" + }, + "lan_ctrl_config": { + "notice_net_dup": "\r\n**[Aviso]** Detectado que várias interfaces podem estar conectadas à mesma rede, escolha com cuidado.", + "net_unavailable": "Interface indisponível" + } + }, + "miot": { + "client": { + "invalid_oauth_info": "Informações de autenticação inválidas, a conexão na nuvem ficará indisponível. Por favor, acesse a página de integração do Xiaomi Home e clique em 'Opções' para autenticar novamente.", + "invalid_device_cache": "Erro no cache de informações do dispositivo. Por favor, acesse a página de integração do Xiaomi Home e clique em 'Opções -> Atualizar lista de dispositivos' para atualizar as informações locais.", + "invalid_cert_info": "Certificado de usuário inválido, a conexão com a central local ficará indisponível. Por favor, acesse a página de integração do Xiaomi Home e clique em 'Opções' para autenticar novamente.", + "device_cloud_error": "Erro ao obter informações do dispositivo na nuvem. Verifique a conexão de rede local.", + "xiaomi_home_error_title": "Erro de integração do Xiaomi Home", + "xiaomi_home_error": "Detectado erro em **{nick_name}({uid}, {cloud_server})**. Por favor, acesse a página de opções para reconfigurar.\n\n**Informação do erro**: \n{message}", + "device_list_changed_title": "Mudança na lista de dispositivos do Xiaomi Home", + "device_list_changed": "Detectada alteração nas informações do dispositivo de **{nick_name}({uid}, {cloud_server})**. Por favor, acesse a página de opções de integração e clique em 'Opções -> Atualizar lista de dispositivos' para atualizar as informações locais.\n\nStatus atual da rede: {network_status}\n{message}\n", + "device_list_add": "\n**{count} novos dispositivos**: \n{message}", + "device_list_del": "\n**{count} dispositivos indisponíveis**: \n{message}", + "device_list_offline": "\n**{count} dispositivos offline**: \n{message}", + "network_status_online": "Online", + "network_status_offline": "Offline", + "device_exec_error": "Erro de execução" + } + }, + "error": { + "common": { + "-10000": "Erro desconhecido", + "-10001": "Serviço indisponível", + "-10002": "Parâmetro inválido", + "-10003": "Recursos insuficientes", + "-10004": "Erro interno", + "-10005": "Permissão negada", + "-10006": "Tempo limite de execução", + "-10007": "Dispositivo offline ou inexistente", + "-10020": "Não autorizado (OAuth2)", + "-10030": "Token inválido (HTTP)", + "-10040": "Formato de mensagem inválido", + "-10050": "Certificado inválido", + "-704000000": "Erro desconhecido", + "-704010000": "Não autorizado (o dispositivo pode ter sido removido)", + "-704014006": "Descrição do dispositivo não encontrada", + "-704030013": "Propriedade não legível", + "-704030023": "Propriedade não gravável", + "-704030033": "Propriedade não subscritível", + "-704040002": "Serviço inexistente", + "-704040003": "Propriedade inexistente", + "-704040004": "Evento inexistente", + "-704040005": "Ação inexistente", + "-704040999": "Funcionalidade não disponível", + "-704042001": "Dispositivo inexistente", + "-704042011": "Dispositivo offline", + "-704053036": "Tempo limite de operação do dispositivo", + "-704053100": "O dispositivo não pode executar esta operação no estado atual", + "-704083036": "Tempo limite de operação do dispositivo", + "-704090001": "Dispositivo inexistente", + "-704220008": "ID inválido", + "-704220025": "Número de parâmetros da ação não corresponde", + "-704220035": "Erro nos parâmetros da ação", + "-704220043": "Valor de propriedade inválido", + "-704222034": "Erro no valor de retorno da ação", + "-705004000": "Erro desconhecido", + "-705004501": "Erro desconhecido", + "-705201013": "Propriedade não legível", + "-705201015": "Erro na execução da ação", + "-705201023": "Propriedade não gravável", + "-705201033": "Propriedade não subscritível", + "-706012000": "Erro desconhecido", + "-706012013": "Propriedade não legível", + "-706012015": "Erro na execução da ação", + "-706012023": "Propriedade não gravável", + "-706012033": "Propriedade não subscritível", + "-706012043": "Valor de propriedade inválido", + "-706014006": "Descrição do dispositivo não encontrada" + } + } +} \ No newline at end of file From 2c37fb63ca94909602e9c7634f626f0a5f2452c6 Mon Sep 17 00:00:00 2001 From: topsworld Date: Wed, 18 Dec 2024 20:39:48 +0800 Subject: [PATCH 19/44] doc: update change log and version --- custom_components/xiaomi_home/manifest.json | 2 +- doc/CHANGELOG.md | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/custom_components/xiaomi_home/manifest.json b/custom_components/xiaomi_home/manifest.json index 67ff027..406fe9d 100644 --- a/custom_components/xiaomi_home/manifest.json +++ b/custom_components/xiaomi_home/manifest.json @@ -25,7 +25,7 @@ "cryptography", "psutil" ], - "version": "v0.1.0", + "version": "v0.1.2", "zeroconf": [ "_miot-central._tcp.local." ] diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 4fc27b4..a05915e 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -1,5 +1,23 @@ # CHANGELOG +## 0.1.2 +### Added +- Support Xiaomi Heater devices. https://github.com/XiaoMi/ha_xiaomi_home/issues/124 https://github.com/XiaoMi/ha_xiaomi_home/issues/117 +- Language supports pt, pt-BR. +### Changed +- Adjust the minimum version of HASS core to 2024.4.4 and above versions. +### Fixed + +## 0.1.1 +### Added +### Changed +### Fixed +- fix humidifier trans rule. https://github.com/XiaoMi/ha_xiaomi_home/issues/59 +- fix get homeinfo error. https://github.com/XiaoMi/ha_xiaomi_home/issues/22 +- fix air-conditioner switch on. https://github.com/XiaoMi/ha_xiaomi_home/issues/37 https://github.com/XiaoMi/ha_xiaomi_home/issues/16 +- fix invalid cover status. https://github.com/XiaoMi/ha_xiaomi_home/issues/11 https://github.com/XiaoMi/ha_xiaomi_home/issues/85 +- water heater entity add STATE_OFF. https://github.com/XiaoMi/ha_xiaomi_home/issues/105 https://github.com/XiaoMi/ha_xiaomi_home/issues/17 + ## 0.1.0 ### Added - first version From fce67ba2da8058c5364a2bfcfe579f1ca5e1773a Mon Sep 17 00:00:00 2001 From: topsworld Date: Wed, 18 Dec 2024 20:45:07 +0800 Subject: [PATCH 20/44] doc: move CONTRIBUTING.md, CHANGELOG.md to root path --- doc/CHANGELOG.md => CHANGELOG.md | 6 +++--- doc/CONTRIBUTING.md => CONTRIBUTING.md | 2 +- README.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename doc/CHANGELOG.md => CHANGELOG.md (96%) rename doc/CONTRIBUTING.md => CONTRIBUTING.md (98%) diff --git a/doc/CHANGELOG.md b/CHANGELOG.md similarity index 96% rename from doc/CHANGELOG.md rename to CHANGELOG.md index a05915e..ed98f74 100644 --- a/doc/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # CHANGELOG -## 0.1.2 +## v0.1.2 ### Added - Support Xiaomi Heater devices. https://github.com/XiaoMi/ha_xiaomi_home/issues/124 https://github.com/XiaoMi/ha_xiaomi_home/issues/117 - Language supports pt, pt-BR. @@ -8,7 +8,7 @@ - Adjust the minimum version of HASS core to 2024.4.4 and above versions. ### Fixed -## 0.1.1 +## v0.1.1 ### Added ### Changed ### Fixed @@ -18,7 +18,7 @@ - fix invalid cover status. https://github.com/XiaoMi/ha_xiaomi_home/issues/11 https://github.com/XiaoMi/ha_xiaomi_home/issues/85 - water heater entity add STATE_OFF. https://github.com/XiaoMi/ha_xiaomi_home/issues/105 https://github.com/XiaoMi/ha_xiaomi_home/issues/17 -## 0.1.0 +## v0.1.0 ### Added - first version ### Changed diff --git a/doc/CONTRIBUTING.md b/CONTRIBUTING.md similarity index 98% rename from doc/CONTRIBUTING.md rename to CONTRIBUTING.md index ff13ba1..dfcdb65 100644 --- a/doc/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contribution Guidelines -[English](./CONTRIBUTING.md) | [简体中文](./CONTRIBUTING_zh.md) +[English](./CONTRIBUTING.md) | [简体中文](./doc/CONTRIBUTING_zh.md) Thank you for considering contributing to our project! We appreciate your efforts to make our project better. diff --git a/README.md b/README.md index 6f97f2a..c140586 100644 --- a/README.md +++ b/README.md @@ -376,8 +376,8 @@ Example: ## Documents - [License](./LICENSE.md) -- Contribution Guidelines: [English](./doc/CONTRIBUTING.md) | [简体中文](./doc/CONTRIBUTING_zh.md) -- [ChangeLog](./doc/CHANGELOG.md) +- Contribution Guidelines: [English](./CONTRIBUTING.md) | [简体中文](./doc/CONTRIBUTING_zh.md) +- [ChangeLog](./CHANGELOG.md) - Development Documents: https://developers.home-assistant.io/docs/creating_component_index ## Directory Structure From 7618f6b366857a81d63d48a1f1e2074458e32834 Mon Sep 17 00:00:00 2001 From: topsworld Date: Wed, 18 Dec 2024 20:46:32 +0800 Subject: [PATCH 21/44] doc: update change log --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed98f74..a14aec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,14 +12,14 @@ ### Added ### Changed ### Fixed -- fix humidifier trans rule. https://github.com/XiaoMi/ha_xiaomi_home/issues/59 -- fix get homeinfo error. https://github.com/XiaoMi/ha_xiaomi_home/issues/22 -- fix air-conditioner switch on. https://github.com/XiaoMi/ha_xiaomi_home/issues/37 https://github.com/XiaoMi/ha_xiaomi_home/issues/16 -- fix invalid cover status. https://github.com/XiaoMi/ha_xiaomi_home/issues/11 https://github.com/XiaoMi/ha_xiaomi_home/issues/85 -- water heater entity add STATE_OFF. https://github.com/XiaoMi/ha_xiaomi_home/issues/105 https://github.com/XiaoMi/ha_xiaomi_home/issues/17 +- Fix humidifier trans rule. https://github.com/XiaoMi/ha_xiaomi_home/issues/59 +- Fix get homeinfo error. https://github.com/XiaoMi/ha_xiaomi_home/issues/22 +- Fix air-conditioner switch on. https://github.com/XiaoMi/ha_xiaomi_home/issues/37 https://github.com/XiaoMi/ha_xiaomi_home/issues/16 +- Fix invalid cover status. https://github.com/XiaoMi/ha_xiaomi_home/issues/11 https://github.com/XiaoMi/ha_xiaomi_home/issues/85 +- Water heater entity add STATE_OFF. https://github.com/XiaoMi/ha_xiaomi_home/issues/105 https://github.com/XiaoMi/ha_xiaomi_home/issues/17 ## v0.1.0 ### Added -- first version +- First version ### Changed ### Fixed From 01f6bbf2c7a29157b4d49f56aad7b0f61cb94a5f Mon Sep 17 00:00:00 2001 From: Rofl <88626207+wheresrofl@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:06:00 +0000 Subject: [PATCH 22/44] feat: nederland translation --- .../xiaomi_home/translations/nl.json | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 custom_components/xiaomi_home/translations/nl.json diff --git a/custom_components/xiaomi_home/translations/nl.json b/custom_components/xiaomi_home/translations/nl.json new file mode 100644 index 0000000..aa15df8 --- /dev/null +++ b/custom_components/xiaomi_home/translations/nl.json @@ -0,0 +1,144 @@ +{ + "config": { + "flow_title": "Xiaomi Home Integratie", + "step": { + "eula": { + "title": "Risiconotitie", + "description": "1. Uw Xiaomi-gebruikersinformatie en apparaatinformatie worden opgeslagen in het Home Assistant-systeem. **Xiaomi kan de beveiliging van het opslagmechanisme van Home Assistant niet garanderen**. U bent verantwoordelijk voor het voorkomen dat uw informatie wordt gestolen.\r\n2. Deze integratie wordt onderhouden door de open-sourcegemeenschap. Er kunnen stabiliteitsproblemen of andere problemen optreden. Bij problemen of fouten met deze integratie, **moet u hulp zoeken bij de open-sourcegemeenschap in plaats van contact op te nemen met de Xiaomi klantenservice**.\r\n3. U heeft enige technische vaardigheden nodig om uw lokale werkomgeving te onderhouden. De integratie is niet gebruiksvriendelijk voor beginners.\r\n4. Lees het README-bestand voordat u begint.\n\n5. Om een stabiel gebruik van de integratie te waarborgen en misbruik van de interface te voorkomen, **mag deze integratie alleen worden gebruikt in Home Assistant. Voor details, zie de LICENSE**.", + "data": { + "eula": "Ik ben me bewust van de bovenstaande risico's en bereid om vrijwillig alle risico's die gepaard gaan met het gebruik van de integratie te aanvaarden." + } + }, + "auth_config": { + "title": "Basisconfiguratie", + "description": "### Inlogregio\r\nSelecteer de regio van uw Xiaomi-account. U kunt deze vinden in de Xiaomi Home APP > Profiel (onderin het menu) > Extra instellingen > Over Xiaomi Home.\r\n### Taal\r\nKies de taal voor de apparaats- en entiteitsnamen. Sommige zinnen zonder vertaling worden in het Engels weergegeven.\r\n### OAuth2 Omleidings-URL\r\nHet OAuth2 authenticatie omleidingsadres is **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant moet zich in hetzelfde lokale netwerk bevinden als de huidige werkterminal (bijv. de persoonlijke computer) en de werkterminal moet toegang hebben tot de startpagina van Home Assistant via dit adres. Anders kan de inlogauthenticatie mislukken.\r\n### Opmerking\r\n- Voor gebruikers met honderden of meer Mi Home-apparaten kan het aanvankelijke toevoegen van de integratie enige tijd duren. Wees geduldig.\r\n- Als Home Assistant draait in een Docker-omgeving, zorg er dan voor dat de Docker-netwerkmodus is ingesteld op host, anders werkt de lokale controlefunctionaliteit mogelijk niet correct.\r\n- De lokale controlefunctionaliteit van de integratie heeft enkele afhankelijkheden. Lees het README zorgvuldig.", + "data": { + "cloud_server": "Inlogregio", + "integration_language": "Taal", + "oauth_redirect_url": "OAuth2 Omleidings-URL" + } + }, + "oauth_error": { + "title": "Inlogfout", + "description": "Klik OP VOLGENDE om het opnieuw te proberen." + }, + "devices_filter": { + "title": "Selecteer Huis en Apparaten", + "description": "## Gebruiksinstructies\r\n### Controlemodus\r\n- Auto: Wanneer er een beschikbare Xiaomi centrale hubgateway in het lokale netwerk is, geeft Home Assistant de voorkeur aan het verzenden van apparaatbedieningscommando's via de centrale hubgateway om lokale controle te bereiken. Als er geen centrale hubgateway in het lokale netwerk is, zal het proberen bedieningscommando's te verzenden via de Xiaomi LAN-controlefunctie. Alleen wanneer de bovenstaande lokale controlevoorwaarden niet zijn vervuld, worden de apparaatbedieningscommando's via de cloud verzonden.\r\n- Cloud: Alle bedieningscommando's worden via de cloud verzonden.\r\n### Apparaten importeren vanuit huis\r\nDe integratie voegt apparaten toe van de geselecteerde huizen.\n### Ruimtenaamsynchronisatiemodus\nBij het importeren van apparaten vanuit de Xiaomi Home APP naar Home Assistant is de naamgevingsconventie van het gebied waarin het apparaat wordt toegevoegd als volgt. Opmerking: het synchronisatieproces van het apparaat verandert de huis- of ruimte-instellingen in de Xiaomi Home APP niet.\r\n- Niet synchroniseren: Het apparaat wordt aan geen enkel gebied toegevoegd.\r\n- Andere opties: Het apparaat wordt toegevoegd aan een gebied dat is genoemd naar de huis- en/of ruimtenamen die al bestaan in de Xiaomi Home APP.\r\n### Debugmodus voor actie\r\nVoor de actie gedefinieerd in MIoT-Spec-V2 van het apparaat, wordt er een Tekstentiteit samen met een Notificatie-entiteit aangemaakt, waarin u bedieningscommando's naar het apparaat kunt sturen voor debugging.\r\n### Verberg niet-standaard gemaakte entiteiten\r\nVerberg de entiteiten die zijn gegenereerd vanuit niet-standaard MIoT-Spec-V2-instanties, waarvan de namen beginnen met \"*\".\r\n\r\n \r\n### Hallo {nick_name}, selecteer alstublieft de integratie controlemethodiek en het huis waar het apparaat dat u wilt importeren zich bevindt.", + "data": { + "ctrl_mode": "Controlemodus", + "home_infos": "Importeer apparaten uit huis", + "area_name_rule": "Ruimtenaamsynchronisatiemodus", + "action_debug": "Debugmodus voor actie", + "hide_non_standard_entities": "Verberg niet-standaard gemaakte entiteiten" + } + } + }, + "progress": { + "oauth": "### {link_left}Klik hier om in te loggen{link_right}\r\n(U wordt automatisch doorgestuurd naar de volgende pagina na succesvolle inlog)" + }, + "error": { + "eula_not_agree": "Lees de risiconotitie.", + "get_token_error": "Mislukt bij het ophalen van inlogautorisatie-informatie (OAuth-token).", + "get_homeinfo_error": "Mislukt bij het ophalen van huisinformatie.", + "mdns_discovery_error": "Lokaal apparaatsontdekkingsservice-exceptie.", + "get_cert_error": "Mislukt bij het ophalen van het certificaat van de centrale hubgateway.", + "no_family_selected": "Geen huis geselecteerd.", + "no_devices": "Het geselecteerde huis heeft geen apparaten. Kies a.u.b. een huis met apparaten en ga verder.", + "no_central_device": "[Centrale Hub Gateway Modus] vereist een beschikbare Xiaomi centrale hubgateway in het lokale netwerk waar Home Assistant zich bevindt. Controleer of het geselecteerde huis aan deze vereiste voldoet." + }, + "abort": { + "network_connect_error": "Configuratie mislukt. De netwerkverbinding is abnormaal. Controleer de netwerkinstellingen van de apparatuur.", + "already_configured": "Configuratie voor deze gebruiker is al voltooid. Ga naar de integratiepagina en klik op de CONFIGUREER-knop om wijzigingen aan te brengen.", + "invalid_auth_info": "Authenticatie-informatie is verlopen. Ga naar de integratiepagina en klik op de CONFIGUREER-knop om opnieuw te authentiseren.", + "config_flow_error": "Integratie configuratiefout: {error}." + } + }, + "options": { + "step": { + "auth_config": { + "title": "Authenticatieconfiguratie", + "description": "Lokale authenticatie-informatie is verlopen. Begin alstublieft het authenticatieproces opnieuw.\r\n### Huidige inlogregio: {cloud_server}\r\n### OAuth2 Omleidings-URL\r\nHet OAuth2 authenticatie omleidingsadres is **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant moet zich in hetzelfde lokale netwerk bevinden als de huidige werkterminal (bijv. de persoonlijke computer) en de werkterminal moet toegang hebben tot de startpagina van Home Assistant via dit adres. Anders kan de inlogauthenticatie mislukken.", + "data": { + "oauth_redirect_url": "OAuth2 Omleidings-URL" + } + }, + "oauth_error": { + "title": "Er is een fout opgetreden tijdens het inloggen.", + "description": "Klik OP VOLGENDE om opnieuw te proberen." + }, + "config_options": { + "title": "Configuratie-opties", + "description": "### Hallo, {nick_name}\r\n\r\nXiaomi ID: {uid}\r\nHuidige inlogregio: {cloud_server}\r\n\r\nKies de opties die u wilt configureren en klik vervolgens op VOLGENDE.", + "data": { + "integration_language": "Integratietaal", + "update_user_info": "Werk gebruikersinformatie bij", + "update_devices": "Werk apparatenlijst bij", + "action_debug": "Debugmodus voor actie", + "hide_non_standard_entities": "Verberg niet-standaard gemaakte entiteiten", + "update_trans_rules": "Werk entiteitsconversieregels bij", + "update_lan_ctrl_config": "Werk LAN controleconfiguratie bij" + } + }, + "update_user_info": { + "title": "Bijwerken van gebruikersnickname", + "description": "Hallo {nick_name}, u kunt uw aangepaste bijnaam hieronder wijzigen.", + "data": { + "nick_name": "Bijnaam" + } + }, + "devices_filter": { + "title": "Huis en Apparaten opnieuw selecteren", + "description": "## Gebruiksinstructies\r\n### Controlemodus\r\n- Auto: Wanneer er een beschikbare Xiaomi centrale hubgateway in het lokale netwerk is, geeft Home Assistant de voorkeur aan het verzenden van apparaatbedieningscommando's via de centrale hubgateway om lokale controle te bereiken. Als er geen centrale hubgateway in het lokale netwerk is, zal het proberen bedieningscommando's te verzenden via de Xiaomi LAN-controlefunctie. Alleen wanneer de bovenstaande lokale controlevoorwaarden niet zijn vervuld, worden de apparaatbedieningscommando's via de cloud verzonden.\r\n- Cloud: Alle bedieningscommando's worden via de cloud verzonden.\r\n### Apparaten importeren vanuit huis\r\nDe integratie voegt apparaten toe van de geselecteerde huizen.\r\n \r\n### Hallo {nick_name}, selecteer alstublieft de integratie controlemethodiek en het huis waar het apparaat dat u wilt importeren zich bevindt.", + "data": { + "ctrl_mode": "Controlemodus", + "home_infos": "Importeer apparaten uit huis" + } + }, + "update_trans_rules": { + "title": "Bijwerken van entiteiten transformateregels", + "description": "## Gebruiksinstructies\r\n- Werk de entiteitsinformatie van apparaten in de huidige integratie-instantie bij, inclusief MIoT-Spec-V2 meertalige configuratie, booleanvertaling en modelfiltering.\r\n- **Waarschuwing**: Dit is een globale configuratie en zal de lokale cache bijwerken. Dit zal alle integratie-instanties beïnvloeden.\r\n- Deze handeling duurt enige tijd, wees geduldig. Vink \"Bevestig bijwerken\" aan en klik op \"Volgende\" om **{urn_count}** regels bij te werken, anders overslaan.", + "data": { + "confirm": "Bevestig de update" + } + }, + "update_lan_ctrl_config": { + "title": "Update LAN controleconfiguratie", + "description": "## Gebruiksinstructies\r\nWerk de configuraties voor de Xiaomi LAN controlefunctie bij. Wanneer de cloud en de centrale hubgateway de apparaten niet kunnen bedienen, zal de integratie proberen de apparaten via het LAN te bedienen. Als er geen netwerkkaart is geselecteerd, zal de LAN controlefunctie niet werken.\r\n- Alleen MIoT-Spec-V2 compatibele IP-apparaten in het LAN worden ondersteund. Sommige apparaten die vóór 2020 zijn geproduceerd, ondersteunen mogelijk geen LAN controle of LAN abonnement.\r\n- Selecteer de netwerkkaart(en) op hetzelfde netwerk als de te bedienen apparaten. Meerdere netwerkkaarten kunnen worden geselecteerd. Als Home Assistant vanwege de meervoudige selectie van de netwerkkaarten twee of meer verbindingen heeft met het lokale netwerk, wordt aanbevolen om de verbinding met de beste netwerkverbinding te selecteren, anders kan dit een negatief effect hebben op de apparaten.\r\n- Als er terminalapparaten (Xiaomi-luidsprekers met scherm, mobiele telefoons, enz.) in het LAN zijn die lokale controle ondersteunen, kan het inschakelen van LAN-abonnement leiden tot lokale automatisering- en apparaatanomalieën.\r\n- **Waarschuwing**: Dit is een globale configuratie. Het zal alle integratie-instanties beïnvloeden. Gebruik het met voorzichtigheid.\r\n{notice_net_dup}", + "data": { + "net_interfaces": "Selecteer alstublieft de te gebruiken netwerkkaart", + "enable_subscribe": "Zet LAN-abonnement aan" + } + }, + "config_confirm": { + "title": "Bevestig Configuratie", + "description": "Hallo **{nick_name}**, bevestig alstublieft de nieuwste configuratie-informatie en klik vervolgens op INDENKEN.\r\nDe integratie zal opnieuw laden met de bijgewerkte configuratie.\r\n\r\nIntegratietaal: \t{lang_new}\r\nBijnaam: \t{nick_name_new}\r\nDebugmodus voor actie: \t{action_debug}\r\nVerberg niet-standaard gemaakte entiteiten: \t{hide_non_standard_entities}\r\nWijzigingen in apparaten: \tVoeg **{devices_add}** apparaten toe, Verwijder **{devices_remove}** apparaten\r\nWijzigingen in transformateregels: \tEr zijn in totaal **{trans_rules_count}** regels, en **{trans_rules_count_success}** regels zijn bijgewerkt", + "data": { + "confirm": "Bevestig de wijziging" + } + } + }, + "progress": { + "oauth": "### {link_left}Klik hier om opnieuw in te loggen{link_right}" + }, + "error": { + "not_auth": "Niet geauthenticeerd. Klik op de authenticatielink om de gebruikersidentiteit te verifiëren.", + "get_token_error": "Mislukt bij het ophalen van inlogautorisatie-informatie (OAuth-token).", + "get_homeinfo_error": "Mislukt bij het ophalen van huisinformatie.", + "get_cert_error": "Mislukt bij het ophalen van het certificaat van de centrale hubgateway.", + "no_devices": "Het geselecteerde huis heeft geen apparaten. Kies a.u.b. een huis met apparaten en ga verder.", + "no_family_selected": "Geen huis geselecteerd.", + "no_central_device": "[Centrale Hub Gateway Modus] vereist een beschikbare Xiaomi centrale hubgateway in het lokale netwerk waar Home Assistant zich bevindt. Controleer of het geselecteerde huis aan deze vereiste voldoet.", + "mdns_discovery_error": "Lokaal apparaatsontdekkingsservice-exceptie.", + "update_config_error": "Mislukt bij het bijwerken van configuratie-informatie.", + "not_confirm": "Wijzigingen zijn niet bevestigd. Bevestig de wijziging voordat u deze indient." + }, + "abort": { + "network_connect_error": "Configuratie mislukt. De netwerkverbinding is abnormaal. Controleer de netwerkinstellingen van de apparatuur.", + "options_flow_error": "Integratie herconfiguratiefout: {error}", + "re_add": "Voeg de integratie opnieuw toe. Foutmelding: {error}", + "storage_error": "Integratie opslagmodule-exceptie. Probeer het opnieuw of voeg de integratie opnieuw toe: {error}", + "inconsistent_account": "Accountinformatie is inconsistent. Log in met het juiste account." + } + } +} From 1e0ddc1f0cbab5c849068a1c3bc30e4d82351613 Mon Sep 17 00:00:00 2001 From: topsworld Date: Wed, 18 Dec 2024 21:20:51 +0800 Subject: [PATCH 23/44] feat: const.INTEGRATION_LANGUAGES add pt, pt-BR --- custom_components/xiaomi_home/miot/const.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/custom_components/xiaomi_home/miot/const.py b/custom_components/xiaomi_home/miot/const.py index 7aec73d..7518ca2 100644 --- a/custom_components/xiaomi_home/miot/const.py +++ b/custom_components/xiaomi_home/miot/const.py @@ -110,11 +110,13 @@ 'zh-Hans': '简体中文', 'zh-Hant': '繁體中文', 'en': 'English', + 'de': 'Deutsch', 'es': 'Español', - 'ru': 'Русский', 'fr': 'Français', - 'de': 'Deutsch', - 'ja': '日本語' + 'ja': '日本語', + 'pt': 'Português', + 'pt-BR': 'Português (Brasil)', + 'ru': 'Русский', } DEFAULT_CTRL_MODE: str = 'auto' From be627afe54ce6c680e5bec4500a247c05198936f Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Wed, 18 Dec 2024 21:34:04 +0800 Subject: [PATCH 24/44] docs: add i18n in README --- README.md | 2 +- doc/README_zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c140586..fe3b75c 100644 --- a/README.md +++ b/README.md @@ -323,7 +323,7 @@ Device information service (urn:miot-spec-v2:service:device-information:00007801 ## Multiple Language Support -There are 8 languages available for selection in the config flow language option of Xiaomi Home, including Simplified Chinese, Traditional Chinese, English, Spanish, Russian, French, German, and Japanese. The config flow page in Simplified Chinese and English has been manually reviewed by the developer. Other languages are translated by machine translation. If you want to modify the words and sentences in the config flow page, you need to modify the json file of the certain language in `custom_components/xiaomi_home/translations/` directory. +There are 8 languages available for selection in the config flow language option of Xiaomi Home, including Simplified Chinese, Traditional Chinese, English, Spanish, Russian, French, German, and Japanese. The config flow page in Simplified Chinese and English has been manually reviewed by the developer. Other languages are translated by machine translation. If you want to modify the words and sentences in the config flow page, you need to modify the json file of the certain language in `custom_components/xiaomi_home/translations/` and `custom_components/xiaomi_home/miot/i18n/` directory. When displaying Home Assistant entity name, Xiaomi Home downloads the multiple language file configured by the device vendor from MIoT Cloud, which contains translations for MIoT-Spec-V2 instances of the device. `multi_lang.json` is a locally maintained multiple language dictionary, which has a higher priority than the multiple language file obtained from the cloud and can be used to supplement or modify the multiple language translation of devices. diff --git a/doc/README_zh.md b/doc/README_zh.md index cee5c67..742c0ea 100644 --- a/doc/README_zh.md +++ b/doc/README_zh.md @@ -325,7 +325,7 @@ event instance name 下的值表示转换后实体所用的 `_attr_device_class` ## 多语言支持 -米家集成配置选项中可选择的集成使用的语言有简体中文、繁体中文、英文、西班牙语、俄语、法语、德语、日语这八种语言。目前,米家集成配置页面的简体中文和英文已经过人工校审,其他语言由机器翻译。如果您希望修改配置页面的词句,则需要修改 `custom_components/xiaomi_home/translations/` 目录下相应语言的 json 文件。 +米家集成配置选项中可选择的集成使用的语言有简体中文、繁体中文、英文、西班牙语、俄语、法语、德语、日语这八种语言。目前,米家集成配置页面的简体中文和英文已经过人工校审,其他语言由机器翻译。如果您希望修改配置页面的词句,则需要修改 `custom_components/xiaomi_home/translations/` 以及 `custom_components/xiaomi_home/miot/i18n/` 目录下相应语言的 json 文件。 在显示 Home Assistant 实体名称时,米家集成会从小米云下载设备厂商为设备配置的多语言文件,该文件包含设备 MIoT-Spec-V2 实例的多语言翻译。 `multi_lang.json` 是本地维护的多语言配置字典,其优先级高于从云端获取的多语言文件,可用于补充或修改设备的多语言翻译。 From 6fae26a378cbcb3bd507b150529b27b7c9e056c6 Mon Sep 17 00:00:00 2001 From: yejinze Date: Wed, 18 Dec 2024 22:01:10 +0800 Subject: [PATCH 25/44] fix: directory copy issue --- install.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index f625e34..5873f13 100755 --- a/install.sh +++ b/install.sh @@ -20,8 +20,13 @@ rm -rf "$config_path/custom_components/xiaomi_home" script_path=$(dirname "$0") # Change to the script path. cd "$script_path" + # Copy the new version. -cp -r custom_components/xiaomi_home/ "$config_path/custom_components/" +if [ -d "$config_path/custom_components" ]; then + cp -r custom_components/xiaomi_home/ "$config_path/custom_components/" +else + cp -r custom_components/ "$config_path/custom_components/" +fi # Done. echo "Xiaomi Home installation is completed. Please restart Home Assistant." From d980b1bfb4136f033b70d482ba8f1d03f42a6728 Mon Sep 17 00:00:00 2001 From: Rofl <88626207+wheresrofl@users.noreply.github.com> Date: Wed, 18 Dec 2024 18:04:23 +0100 Subject: [PATCH 26/44] feat: more dutch translations accidentally missed this file --- .../xiaomi_home/miot/i18n/nl.json | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 custom_components/xiaomi_home/miot/i18n/nl.json diff --git a/custom_components/xiaomi_home/miot/i18n/nl.json b/custom_components/xiaomi_home/miot/i18n/nl.json new file mode 100644 index 0000000..03dfc32 --- /dev/null +++ b/custom_components/xiaomi_home/miot/i18n/nl.json @@ -0,0 +1,95 @@ +{ + "config": { + "other": { + "devices": "Apparaten", + "found_central_gateway": ", Lokale centrale hub-gateway gevonden" + }, + "control_mode": { + "auto": "Automatisch", + "cloud": "Cloud" + }, + "room_name_rule": { + "none": "Niet synchroniseren", + "home_room": "Huisnaam en Kamernaam (Xiaomi Home Slaapkamer)", + "room": "Kamernaam (Slaapkamer)", + "home": "Huisnaam (Xiaomi Home)" + }, + "option_status": { + "enable": "Inschakelen", + "disable": "Uitschakelen" + }, + "lan_ctrl_config": { + "notice_net_dup": "\r\n**[Let op]** Meerdere netwerkkaarten gedetecteerd die mogelijk zijn verbonden met hetzelfde netwerk. Let op bij de selectie.", + "net_unavailable": "Interface niet beschikbaar" + } + }, + "miot": { + "client": { + "invalid_oauth_info": "Authenticatie-informatie is ongeldig, cloudverbinding zal niet beschikbaar zijn. Ga naar de Xiaomi Home-integratiepagina en klik op 'Opties' om opnieuw te verifiëren.", + "invalid_device_cache": "Cache van apparaatgegevens is abnormaal. Ga naar de Xiaomi Home-integratiepagina, klik op 'Opties->Apparaatlijst bijwerken' en werk de lokale cache bij.", + "invalid_cert_info": "Ongeldig gebruikerscertificaat, lokale centrale verbinding zal niet beschikbaar zijn. Ga naar de Xiaomi Home-integratiepagina en klik op 'Opties' om opnieuw te verifiëren.", + "device_cloud_error": "Er is een uitzondering opgetreden bij het ophalen van apparaatgegevens uit de cloud. Controleer de lokale netwerkverbinding.", + "xiaomi_home_error_title": "Xiaomi Home-integratiefout", + "xiaomi_home_error": "Gedetecteerd **{nick_name}({uid}, {cloud_server})** fout, ga naar de optiespagina om opnieuw te configureren.\n\n**Foutmelding**: \n{message}", + "device_list_changed_title": "Wijzigingen in Xiaomi Home-apparaatlijst", + "device_list_changed": "Gedetecteerd **{nick_name}({uid}, {cloud_server})** apparaatgegevens zijn gewijzigd. Ga naar de integratie-optiespagina, klik op `Opties->Apparaatlijst bijwerken` en werk lokale apparaatgegevens bij.\n\nHuidige netwerkstatus: {network_status}\n{message}\n", + "device_list_add": "\n**{count} nieuwe apparaten:** \n{message}", + "device_list_del": "\n**{count} apparaten niet beschikbaar:** \n{message}", + "device_list_offline": "\n**{count} apparaten offline:** \n{message}", + "network_status_online": "Online", + "network_status_offline": "Offline", + "device_exec_error": "Uitvoeringsfout" + } + }, + "error": { + "common": { + "-10000": "Onbekende fout", + "-10001": "Service niet beschikbaar", + "-10002": "Ongeldige parameter", + "-10003": "Onvoldoende middelen", + "-10004": "Interne fout", + "-10005": "Onvoldoende machtigingen", + "-10006": "Uitvoeringstijd verstreken", + "-10007": "Apparaat offline of bestaat niet", + "-10020": "Niet geautoriseerd (OAuth2)", + "-10030": "Ongeldig token (HTTP)", + "-10040": "Ongeldig berichtformaat", + "-10050": "Ongeldig certificaat", + "-704000000": "Onbekende fout", + "-704010000": "Niet geautoriseerd (apparaat kan zijn verwijderd)", + "-704014006": "Apparaatbeschrijving niet gevonden", + "-704030013": "Eigenschap niet leesbaar", + "-704030023": "Eigenschap niet schrijfbaar", + "-704030033": "Eigenschap niet abonneerbaar", + "-704040002": "Service bestaat niet", + "-704040003": "Eigenschap bestaat niet", + "-704040004": "Gebeurtenis bestaat niet", + "-704040005": "Actie bestaat niet", + "-704040999": "Functie niet online", + "-704042001": "Apparaat bestaat niet", + "-704042011": "Apparaat offline", + "-704053036": "Apparaatbedieningstijd verstreken", + "-704053100": "Apparaat kan deze handeling niet uitvoeren in de huidige staat", + "-704083036": "Apparaatbedieningstijd verstreken", + "-704090001": "Apparaat bestaat niet", + "-704220008": "Ongeldige ID", + "-704220025": "Aantal actieparameters komt niet overeen", + "-704220035": "Fout in actieparameter", + "-704220043": "Fout in eigenschapswaarde", + "-704222034": "Fout in retourwaarde actie", + "-705004000": "Onbekende fout", + "-705004501": "Onbekende fout", + "-705201013": "Eigenschap niet leesbaar", + "-705201015": "Fout bij uitvoeren van actie", + "-705201023": "Eigenschap niet schrijfbaar", + "-705201033": "Eigenschap niet abonneerbaar", + "-706012000": "Onbekende fout", + "-706012013": "Eigenschap niet leesbaar", + "-706012015": "Fout bij uitvoeren van actie", + "-706012023": "Eigenschap niet schrijfbaar", + "-706012033": "Eigenschap niet abonneerbaar", + "-706012043": "Fout in eigenschapswaarde", + "-706014006": "Apparaatbeschrijving niet gevonden" + } + } +} From 8364a544f29e7e90bbdd9513f706ae9fe5d3d5e3 Mon Sep 17 00:00:00 2001 From: flyingfeng <2917268843@qq.com> Date: Thu, 19 Dec 2024 10:03:03 +0800 Subject: [PATCH 27/44] Fix incorrect links --- doc/CONTRIBUTING_zh.md | 2 +- doc/README_zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/CONTRIBUTING_zh.md b/doc/CONTRIBUTING_zh.md index 3d57931..2201fb8 100644 --- a/doc/CONTRIBUTING_zh.md +++ b/doc/CONTRIBUTING_zh.md @@ -1,6 +1,6 @@ # 贡献指南 -[English](./CONTRIBUTING.md) | [简体中文](./CONTRIBUTING_zh.md) +[English](../CONTRIBUTING.md) | [简体中文](./CONTRIBUTING_zh.md) 感谢您考虑为我们的项目做出贡献!您的努力将使我们的项目变得更好。 diff --git a/doc/README_zh.md b/doc/README_zh.md index 742c0ea..3b4ead3 100644 --- a/doc/README_zh.md +++ b/doc/README_zh.md @@ -378,7 +378,7 @@ siid、piid、eiid、aiid、value 均为十进制三位整数。 ## 文档 - [许可证](../LICENSE.md) -- 贡献指南: [English](./CONTRIBUTING.md) | [简体中文](./CONTRIBUTING_zh.md) +- 贡献指南: [English](../CONTRIBUTING.md) | [简体中文](./CONTRIBUTING_zh.md) - [更新日志](./CHANGELOG.md) - 开发文档: https://developers.home-assistant.io/docs/creating_component_index From 3c464a0b0c1c25ba8bb21d3743106cda1e4eac79 Mon Sep 17 00:00:00 2001 From: Feng Wang Date: Thu, 19 Dec 2024 10:58:41 +0800 Subject: [PATCH 28/44] Simplify the install logic. --- install.sh | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/install.sh b/install.sh index 5873f13..85c3ecf 100755 --- a/install.sh +++ b/install.sh @@ -14,19 +14,21 @@ if [ ! -d "$config_path" ]; then exit 1 fi -# Remove the old version. -rm -rf "$config_path/custom_components/xiaomi_home" # Get the script path. script_path=$(dirname "$0") -# Change to the script path. -cd "$script_path" + +# Set source and target +component_name=xiaomi_home +source_path="$script_path/custom_components/$component_name" +target_root="$config_path/custom_components" +target_path="$target_root/$component_name" + +# Remove the old version. +rm -rf "$target_path" # Copy the new version. -if [ -d "$config_path/custom_components" ]; then - cp -r custom_components/xiaomi_home/ "$config_path/custom_components/" -else - cp -r custom_components/ "$config_path/custom_components/" -fi +mkdir -p "$target_root" +cp -r "$source_path" "$target_path" # Done. echo "Xiaomi Home installation is completed. Please restart Home Assistant." From dabf277942ea9b783305726dad7dfc081ec9dabd Mon Sep 17 00:00:00 2001 From: topsworld Date: Thu, 19 Dec 2024 13:30:26 +0800 Subject: [PATCH 29/44] test: check lang_integrity --- test/check_rule_format.py | 96 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/test/check_rule_format.py b/test/check_rule_format.py index db2d829..00ba86a 100644 --- a/test/check_rule_format.py +++ b/test/check_rule_format.py @@ -4,6 +4,7 @@ from os import listdir, path from typing import Optional import pytest +import yaml SOURCE_DIR: str = path.dirname(path.abspath(__file__)) @@ -20,6 +21,18 @@ def load_json_file(file_path: str) -> Optional[dict]: return None +def load_yaml_file(file_path: str) -> Optional[dict]: + try: + with open(file_path, 'r', encoding='utf-8') as file: + return yaml.safe_load(file) + except FileNotFoundError: + print(file_path, 'is not found.') + return None + except yaml.YAMLError: + print(file_path, 'is not a valid YAML file.') + return None + + def dict_str_str(d: dict) -> bool: """restricted format: dict[str, str]""" if not isinstance(d, dict): @@ -86,6 +99,30 @@ def bool_trans(d: dict) -> bool: return True +def compare_dict_structure(dict1: dict, dict2: dict) -> bool: + if not isinstance(dict1, dict) or not isinstance(dict2, dict): + print('invalid type') + return False + if dict1.keys() != dict2.keys(): + print('inconsistent key values, ', dict1.keys(), dict2.keys()) + return False + for key in dict1: + if isinstance(dict1[key], dict) and isinstance(dict2[key], dict): + if not compare_dict_structure(dict1[key], dict2[key]): + print('inconsistent key values, dict, ', key) + return False + elif isinstance(dict1[key], list) and isinstance(dict2[key], list): + if not all( + isinstance(i, type(j)) + for i, j in zip(dict1[key], dict2[key])): + print('inconsistent key values, list, ', key) + return False + elif not isinstance(dict1[key], type(dict2[key])): + print('inconsistent key values, type, ', key) + return False + return True + + @pytest.mark.github def test_bool_trans(): data: dict = load_json_file( @@ -125,3 +162,62 @@ def test_miot_i18n(): data: dict = load_json_file(file_path) assert data assert nested_3_dict_str_str(data) + + +@pytest.mark.github +def test_translations(): + i18n_path: str = path.join( + SOURCE_DIR, '../custom_components/xiaomi_home/translations') + for file_name in listdir(i18n_path): + file_path: str = path.join(i18n_path, file_name) + data: dict = load_json_file(file_path) + assert data + assert dict_str_dict(data) + + +@pytest.mark.github +def test_miot_lang_integrity(): + # pylint: disable=import-outside-toplevel + from miot.const import INTEGRATION_LANGUAGES + integration_lang_list: list[str] = [ + f'{key}.json' for key in list(INTEGRATION_LANGUAGES.keys())] + translations_names: set[str] = set(listdir( + path.join( + SOURCE_DIR, '../custom_components/xiaomi_home/translations'))) + assert len(translations_names) == len(integration_lang_list) + assert translations_names == set(integration_lang_list) + i18n_names: set[str] = set(listdir( + path.join( + SOURCE_DIR, '../custom_components/xiaomi_home/miot/i18n'))) + assert len(i18n_names) == len(translations_names) + assert i18n_names == translations_names + # Check translation files structure + default_dict: dict = load_json_file( + path.join( + SOURCE_DIR, + '../custom_components/xiaomi_home/translations', + integration_lang_list[0])) + for name in list(integration_lang_list)[1:]: + compare_dict: dict = load_json_file( + path.join( + SOURCE_DIR, + '../custom_components/xiaomi_home/translations', + name)) + if not compare_dict_structure(default_dict, compare_dict): + print('compare_dict_structure failed /translations, ', name) + assert False + # Check i18n files structure + default_dict = load_json_file( + path.join( + SOURCE_DIR, + '../custom_components/xiaomi_home/miot/i18n', + integration_lang_list[0])) + for name in list(integration_lang_list)[1:]: + compare_dict: dict = load_json_file( + path.join( + SOURCE_DIR, + '../custom_components/xiaomi_home/miot/i18n', + name)) + if not compare_dict_structure(default_dict, compare_dict): + print('compare_dict_structure failed /miot/i18n, ', name) + assert False From b93d8631b8c079c194f04e4bb0a727b5a37f803d Mon Sep 17 00:00:00 2001 From: topsworld Date: Thu, 19 Dec 2024 13:31:17 +0800 Subject: [PATCH 30/44] fix: translations structure error --- .../xiaomi_home/miot/i18n/zh-Hant.json | 12 +++++------- custom_components/xiaomi_home/translations/ja.json | 4 +--- custom_components/xiaomi_home/translations/ru.json | 14 +++++++------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/custom_components/xiaomi_home/miot/i18n/zh-Hant.json b/custom_components/xiaomi_home/miot/i18n/zh-Hant.json index bf381a3..e62b4f9 100644 --- a/custom_components/xiaomi_home/miot/i18n/zh-Hant.json +++ b/custom_components/xiaomi_home/miot/i18n/zh-Hant.json @@ -43,20 +43,18 @@ }, "error": { "common": { - "-1": "未知錯誤", "-10000": "未知錯誤", "-10001": "服務不可用", - "-10002": "無效參數", + "-10002": "參數無效", "-10003": "資源不足", "-10004": "內部錯誤", "-10005": "權限不足", "-10006": "執行超時", "-10007": "設備離線或者不存在", - "-10020": "無效的消息格式" - }, - "gw": {}, - "lan": {}, - "cloud": { + "-10020": "未授權(OAuth2)", + "-10030": "無效的token(HTTP)", + "-10040": "無效的消息格式", + "-10050": "無效的證書", "-704000000": "未知錯誤", "-704010000": "未授權(設備可能被刪除)", "-704014006": "沒找到設備描述", diff --git a/custom_components/xiaomi_home/translations/ja.json b/custom_components/xiaomi_home/translations/ja.json index 96d8029..780378d 100644 --- a/custom_components/xiaomi_home/translations/ja.json +++ b/custom_components/xiaomi_home/translations/ja.json @@ -45,9 +45,7 @@ "get_cert_error": "ゲートウェイ証明書を取得できませんでした。", "no_family_selected": "家庭が選択されていません。", "no_devices": "選択された家庭にデバイスがありません。デバイスがある家庭を選択して続行してください。", - "no_central_device": "【中央ゲートウェイモード】Home Assistant が存在する LAN 内に使用可能な Xiaomi 中央ゲートウェイがある必要があります。選択された家庭がこの要件を満たしているかどうかを確認してください。", - "update_config_error": "設定情報の更新に失敗しました。", - "not_confirm": "変更項目が確認されていません。確認を選択してから送信してください。" + "no_central_device": "【中央ゲートウェイモード】Home Assistant が存在する LAN 内に使用可能な Xiaomi 中央ゲートウェイがある必要があります。選択された家庭がこの要件を満たしているかどうかを確認してください。" }, "abort": { "network_connect_error": "設定に失敗しました。ネットワーク接続に異常があります。デバイスのネットワーク設定を確認してください。", diff --git a/custom_components/xiaomi_home/translations/ru.json b/custom_components/xiaomi_home/translations/ru.json index 06c49ca..778206d 100644 --- a/custom_components/xiaomi_home/translations/ru.json +++ b/custom_components/xiaomi_home/translations/ru.json @@ -45,13 +45,13 @@ "get_cert_error": "Не удалось получить сертификат центрального шлюза.", "no_family_selected": "Не выбрана домашняя сеть.", "no_devices": "В выбранной домашней сети нет устройств. Пожалуйста, выберите домашнюю сеть с устройствами и продолжайте.", - "no_central_device": "Для режима центрального шлюза Xiaomi необходимо наличие доступного центрального шлюза Xiaomi в локальной сети Home Assistant. Проверьте, соответствует ли выбранная домашняя сеть этому требованию.", - "abort": { - "network_connect_error": "Ошибка настройки. Сетевое подключение недоступно. Проверьте настройки сети устройства.", - "already_configured": "Этот пользователь уже настроен. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы изменить настройки.", - "invalid_auth_info": "Информация об авторизации истекла. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы переавторизоваться.", - "config_flow_error": "Ошибка настройки интеграции: {error}" - } + "no_central_device": "Для режима центрального шлюза Xiaomi необходимо наличие доступного центрального шлюза Xiaomi в локальной сети Home Assistant. Проверьте, соответствует ли выбранная домашняя сеть этому требованию." + }, + "abort": { + "network_connect_error": "Ошибка настройки. Сетевое подключение недоступно. Проверьте настройки сети устройства.", + "already_configured": "Этот пользователь уже настроен. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы изменить настройки.", + "invalid_auth_info": "Информация об авторизации истекла. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы переавторизоваться.", + "config_flow_error": "Ошибка настройки интеграции: {error}" } }, "options": { From 2d6387c30a86c3286a8b95306b551b701121f196 Mon Sep 17 00:00:00 2001 From: topsworld Date: Thu, 19 Dec 2024 13:53:53 +0800 Subject: [PATCH 31/44] test: check_rule_format.py use constant --- test/check_rule_format.py | 41 +++++++++++++-------------------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/test/check_rule_format.py b/test/check_rule_format.py index 00ba86a..48d602e 100644 --- a/test/check_rule_format.py +++ b/test/check_rule_format.py @@ -6,7 +6,9 @@ import pytest import yaml -SOURCE_DIR: str = path.dirname(path.abspath(__file__)) +SOURCE_PATH: str = path.dirname(path.abspath(__file__)) +TRANS_RELATIVE_PATH: str = '../custom_components/xiaomi_home/translations' +MIOT_I18N_RELATIVE_PATH: str = '../custom_components/xiaomi_home/miot/i18n' def load_json_file(file_path: str) -> Optional[dict]: @@ -127,7 +129,7 @@ def compare_dict_structure(dict1: dict, dict2: dict) -> bool: def test_bool_trans(): data: dict = load_json_file( path.join( - SOURCE_DIR, + SOURCE_PATH, '../custom_components/xiaomi_home/miot/specs/bool_trans.json')) assert data assert bool_trans(data) @@ -137,7 +139,7 @@ def test_bool_trans(): def test_spec_filter(): data: dict = load_json_file( path.join( - SOURCE_DIR, + SOURCE_PATH, '../custom_components/xiaomi_home/miot/specs/spec_filter.json')) assert data assert spec_filter(data) @@ -147,7 +149,7 @@ def test_spec_filter(): def test_multi_lang(): data: dict = load_json_file( path.join( - SOURCE_DIR, + SOURCE_PATH, '../custom_components/xiaomi_home/miot/specs/multi_lang.json')) assert data assert nested_3_dict_str_str(data) @@ -155,8 +157,7 @@ def test_multi_lang(): @pytest.mark.github def test_miot_i18n(): - i18n_path: str = path.join( - SOURCE_DIR, '../custom_components/xiaomi_home/miot/i18n') + i18n_path: str = path.join(SOURCE_PATH, MIOT_I18N_RELATIVE_PATH) for file_name in listdir(i18n_path): file_path: str = path.join(i18n_path, file_name) data: dict = load_json_file(file_path) @@ -166,8 +167,7 @@ def test_miot_i18n(): @pytest.mark.github def test_translations(): - i18n_path: str = path.join( - SOURCE_DIR, '../custom_components/xiaomi_home/translations') + i18n_path: str = path.join(SOURCE_PATH, TRANS_RELATIVE_PATH) for file_name in listdir(i18n_path): file_path: str = path.join(i18n_path, file_name) data: dict = load_json_file(file_path) @@ -182,42 +182,29 @@ def test_miot_lang_integrity(): integration_lang_list: list[str] = [ f'{key}.json' for key in list(INTEGRATION_LANGUAGES.keys())] translations_names: set[str] = set(listdir( - path.join( - SOURCE_DIR, '../custom_components/xiaomi_home/translations'))) + path.join(SOURCE_PATH, TRANS_RELATIVE_PATH))) assert len(translations_names) == len(integration_lang_list) assert translations_names == set(integration_lang_list) i18n_names: set[str] = set(listdir( - path.join( - SOURCE_DIR, '../custom_components/xiaomi_home/miot/i18n'))) + path.join(SOURCE_PATH, MIOT_I18N_RELATIVE_PATH))) assert len(i18n_names) == len(translations_names) assert i18n_names == translations_names # Check translation files structure default_dict: dict = load_json_file( - path.join( - SOURCE_DIR, - '../custom_components/xiaomi_home/translations', - integration_lang_list[0])) + path.join(SOURCE_PATH, TRANS_RELATIVE_PATH, integration_lang_list[0])) for name in list(integration_lang_list)[1:]: compare_dict: dict = load_json_file( - path.join( - SOURCE_DIR, - '../custom_components/xiaomi_home/translations', - name)) + path.join(SOURCE_PATH, TRANS_RELATIVE_PATH, name)) if not compare_dict_structure(default_dict, compare_dict): print('compare_dict_structure failed /translations, ', name) assert False # Check i18n files structure default_dict = load_json_file( path.join( - SOURCE_DIR, - '../custom_components/xiaomi_home/miot/i18n', - integration_lang_list[0])) + SOURCE_PATH, MIOT_I18N_RELATIVE_PATH, integration_lang_list[0])) for name in list(integration_lang_list)[1:]: compare_dict: dict = load_json_file( - path.join( - SOURCE_DIR, - '../custom_components/xiaomi_home/miot/i18n', - name)) + path.join(SOURCE_PATH, MIOT_I18N_RELATIVE_PATH, name)) if not compare_dict_structure(default_dict, compare_dict): print('compare_dict_structure failed /miot/i18n, ', name) assert False From f10885fbfdeb29c1646cd56294438c1d494bb562 Mon Sep 17 00:00:00 2001 From: Feng Wang Date: Thu, 19 Dec 2024 17:22:31 +0800 Subject: [PATCH 32/44] Fix dutch translation declaration --- custom_components/xiaomi_home/miot/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/xiaomi_home/miot/const.py b/custom_components/xiaomi_home/miot/const.py index 7518ca2..1bb684a 100644 --- a/custom_components/xiaomi_home/miot/const.py +++ b/custom_components/xiaomi_home/miot/const.py @@ -117,6 +117,7 @@ 'pt': 'Português', 'pt-BR': 'Português (Brasil)', 'ru': 'Русский', + 'nl': 'Nederlands' } DEFAULT_CTRL_MODE: str = 'auto' From 571483b3029b255ccddc4c0ab856c49f1e06e8f9 Mon Sep 17 00:00:00 2001 From: Paul Shawn <32349595+topsworld@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:15:07 +0800 Subject: [PATCH 33/44] chore: Improve multi-language translation actions (#256) * test: check bool trans integrity * feat: add tools to update and format data * style: sort data * feat: update check_rule_format.py * style: remove unuse SUPPORTED_PLATFORMS item * test: check spec rule sort * fix: fix py import * feat: remove unuse code * feat: add spec data sort --- custom_components/xiaomi_home/miot/const.py | 16 +- .../xiaomi_home/miot/specs/bool_trans.json | 230 +++++++++++------- .../xiaomi_home/miot/specs/multi_lang.json | 88 +++---- .../xiaomi_home/miot/specs/spec_filter.json | 46 ++-- test/check_rule_format.py | 163 ++++++++++--- test/pytest.ini | 3 +- 6 files changed, 341 insertions(+), 205 deletions(-) diff --git a/custom_components/xiaomi_home/miot/const.py b/custom_components/xiaomi_home/miot/const.py index 1bb684a..c586992 100644 --- a/custom_components/xiaomi_home/miot/const.py +++ b/custom_components/xiaomi_home/miot/const.py @@ -67,24 +67,16 @@ MANUFACTURER_EFFECTIVE_TIME = 3600*24*14 SUPPORTED_PLATFORMS: list = [ - # 'alarm_control_panel', 'binary_sensor', 'button', 'climate', - # 'camera', - # 'conversation', 'cover', - # 'device_tracker', 'event', 'fan', 'humidifier', 'light', - # 'lock', - # 'media_player', 'notify', 'number', - # 'remote', - # 'scene', 'select', 'sensor', 'switch', @@ -107,17 +99,17 @@ DEFAULT_INTEGRATION_LANGUAGE: str = 'en' INTEGRATION_LANGUAGES = { - 'zh-Hans': '简体中文', - 'zh-Hant': '繁體中文', - 'en': 'English', 'de': 'Deutsch', + 'en': 'English', 'es': 'Español', 'fr': 'Français', 'ja': '日本語', + 'nl': 'Nederlands', 'pt': 'Português', 'pt-BR': 'Português (Brasil)', 'ru': 'Русский', - 'nl': 'Nederlands' + 'zh-Hans': '简体中文', + 'zh-Hant': '繁體中文' } DEFAULT_CTRL_MODE: str = 'auto' diff --git a/custom_components/xiaomi_home/miot/specs/bool_trans.json b/custom_components/xiaomi_home/miot/specs/bool_trans.json index 649310d..0bf193f 100644 --- a/custom_components/xiaomi_home/miot/specs/bool_trans.json +++ b/custom_components/xiaomi_home/miot/specs/bool_trans.json @@ -3,14 +3,20 @@ "urn:miot-spec-v2:property:air-cooler:000000EB": "open_close", "urn:miot-spec-v2:property:alarm:00000012": "open_close", "urn:miot-spec-v2:property:anion:00000025": "open_close", + "urn:miot-spec-v2:property:anti-fake:00000130": "yes_no", + "urn:miot-spec-v2:property:arrhythmia:000000B4": "yes_no", "urn:miot-spec-v2:property:auto-cleanup:00000124": "open_close", "urn:miot-spec-v2:property:auto-deodorization:00000125": "open_close", "urn:miot-spec-v2:property:auto-keep-warm:0000002B": "open_close", "urn:miot-spec-v2:property:automatic-feeding:000000F0": "open_close", "urn:miot-spec-v2:property:blow:000000CD": "open_close", + "urn:miot-spec-v2:property:card-insertion-state:00000106": "yes_no", + "urn:miot-spec-v2:property:contact-state:0000007C": "contact_state", + "urn:miot-spec-v2:property:current-physical-control-lock:00000099": "open_close", + "urn:miot-spec-v2:property:delay:0000014F": "yes_no", "urn:miot-spec-v2:property:deodorization:000000C6": "open_close", "urn:miot-spec-v2:property:dns-auto-mode:000000DC": "open_close", - "urn:miot-spec-v2:property:current-physical-control-lock:00000099": "open_close", + "urn:miot-spec-v2:property:driving-status:000000B9": "yes_no", "urn:miot-spec-v2:property:dryer:00000027": "open_close", "urn:miot-spec-v2:property:eco:00000024": "open_close", "urn:miot-spec-v2:property:glimmer-full-color:00000089": "open_close", @@ -20,17 +26,25 @@ "urn:miot-spec-v2:property:horizontal-swing:00000017": "open_close", "urn:miot-spec-v2:property:hot-water-recirculation:0000011C": "open_close", "urn:miot-spec-v2:property:image-distortion-correction:0000010F": "open_close", - "urn:miot-spec-v2:property:mute:00000040": "open_close", + "urn:miot-spec-v2:property:local-storage:0000011E": "yes_no", "urn:miot-spec-v2:property:motion-detection:00000056": "open_close", + "urn:miot-spec-v2:property:motion-state:0000007D": "motion_state", "urn:miot-spec-v2:property:motion-tracking:0000008A": "open_close", + "urn:miot-spec-v2:property:motor-reverse:00000072": "yes_no", + "urn:miot-spec-v2:property:mute:00000040": "open_close", "urn:miot-spec-v2:property:off-delay:00000053": "open_close", "urn:miot-spec-v2:property:on:00000006": "open_close", "urn:miot-spec-v2:property:physical-controls-locked:0000001D": "open_close", + "urn:miot-spec-v2:property:plasma:00000132": "yes_no", "urn:miot-spec-v2:property:preheat:00000103": "open_close", + "urn:miot-spec-v2:property:seating-state:000000B8": "yes_no", + "urn:miot-spec-v2:property:silent-execution:000000FB": "yes_no", "urn:miot-spec-v2:property:sleep-aid-mode:0000010B": "open_close", "urn:miot-spec-v2:property:sleep-mode:00000028": "open_close", + "urn:miot-spec-v2:property:snore-state:0000012A": "yes_no", "urn:miot-spec-v2:property:soft-wind:000000CF": "open_close", "urn:miot-spec-v2:property:speed-control:000000E8": "open_close", + "urn:miot-spec-v2:property:submersion-state:0000007E": "yes_no", "urn:miot-spec-v2:property:time-watermark:00000087": "open_close", "urn:miot-spec-v2:property:un-straight-blowing:00000100": "open_close", "urn:miot-spec-v2:property:uv:00000029": "open_close", @@ -43,41 +57,19 @@ "urn:miot-spec-v2:property:wdr-mode:00000088": "open_close", "urn:miot-spec-v2:property:wet:0000002A": "open_close", "urn:miot-spec-v2:property:wifi-band-combine:000000E0": "open_close", - "urn:miot-spec-v2:property:anti-fake:00000130": "yes_no", - "urn:miot-spec-v2:property:arrhythmia:000000B4": "yes_no", - "urn:miot-spec-v2:property:card-insertion-state:00000106": "yes_no", - "urn:miot-spec-v2:property:delay:0000014F": "yes_no", - "urn:miot-spec-v2:property:driving-status:000000B9": "yes_no", - "urn:miot-spec-v2:property:local-storage:0000011E": "yes_no", - "urn:miot-spec-v2:property:motor-reverse:00000072": "yes_no", - "urn:miot-spec-v2:property:plasma:00000132": "yes_no", - "urn:miot-spec-v2:property:seating-state:000000B8": "yes_no", - "urn:miot-spec-v2:property:silent-execution:000000FB": "yes_no", - "urn:miot-spec-v2:property:snore-state:0000012A": "yes_no", - "urn:miot-spec-v2:property:submersion-state:0000007E": "yes_no", "urn:miot-spec-v2:property:wifi-ssid-hidden:000000E3": "yes_no", - "urn:miot-spec-v2:property:wind-reverse:00000117": "yes_no", - "urn:miot-spec-v2:property:motion-state:0000007D": "motion_state", - "urn:miot-spec-v2:property:contact-state:0000007C": "contact_state" + "urn:miot-spec-v2:property:wind-reverse:00000117": "yes_no" }, "translate": { "default": { - "zh-Hans": { - "true": "真", - "false": "假" - }, - "zh-Hant": { - "true": "真", - "false": "假" + "de": { + "true": "Wahr", + "false": "Falsch" }, "en": { "true": "True", "false": "False" }, - "de": { - "true": "Wahr", - "false": "Falsch" - }, "es": { "true": "Verdadero", "false": "Falso" @@ -86,32 +78,44 @@ "true": "Vrai", "false": "Faux" }, + "ja": { + "true": "真", + "false": "偽" + }, + "nl": { + "true": "True", + "false": "False" + }, + "pt": { + "true": "True", + "false": "False" + }, + "pt-BR": { + "true": "True", + "false": "False" + }, "ru": { "true": "Истина", "false": "Ложь" }, - "ja": { + "zh-Hans": { "true": "真", - "false": "偽" + "false": "假" + }, + "zh-Hant": { + "true": "真", + "false": "假" } }, "open_close": { - "zh-Hans": { - "true": "开启", - "false": "关闭" - }, - "zh-Hant": { - "true": "開啟", - "false": "關閉" + "de": { + "true": "Öffnen", + "false": "Schließen" }, "en": { "true": "Open", "false": "Close" }, - "de": { - "true": "Öffnen", - "false": "Schließen" - }, "es": { "true": "Abierto", "false": "Cerrado" @@ -120,32 +124,44 @@ "true": "Ouvert", "false": "Fermer" }, + "ja": { + "true": "開く", + "false": "閉じる" + }, + "nl": { + "true": "Open", + "false": "Dicht" + }, + "pt": { + "true": "Aberto", + "false": "Fechado" + }, + "pt-BR": { + "true": "Aberto", + "false": "Fechado" + }, "ru": { "true": "Открыть", "false": "Закрыть" }, - "ja": { - "true": "開く", - "false": "閉じる" - } - }, - "yes_no": { "zh-Hans": { - "true": "是", - "false": "否" + "true": "开启", + "false": "关闭" }, "zh-Hant": { - "true": "是", - "false": "否" + "true": "開啟", + "false": "關閉" + } + }, + "yes_no": { + "de": { + "true": "Ja", + "false": "Nein" }, "en": { "true": "Yes", "false": "No" }, - "de": { - "true": "Ja", - "false": "Nein" - }, "es": { "true": "Sí", "false": "No" @@ -154,32 +170,44 @@ "true": "Oui", "false": "Non" }, + "ja": { + "true": "はい", + "false": "いいえ" + }, + "nl": { + "true": "Ja", + "false": "Nee" + }, + "pt": { + "true": "Sim", + "false": "Não" + }, + "pt-BR": { + "true": "Sim", + "false": "Não" + }, "ru": { "true": "Да", "false": "Нет" }, - "ja": { - "true": "はい", - "false": "いいえ" - } - }, - "motion_state": { "zh-Hans": { - "true": "有人", - "false": "无人" + "true": "是", + "false": "否" }, "zh-Hant": { - "true": "有人", - "false": "無人" + "true": "是", + "false": "否" + } + }, + "motion_state": { + "de": { + "true": "Bewegung erkannt", + "false": "Keine Bewegung erkannt" }, "en": { "true": "Motion Detected", "false": "No Motion Detected" }, - "de": { - "true": "Bewegung erkannt", - "false": "Keine Bewegung erkannt" - }, "es": { "true": "Movimiento detectado", "false": "No se detecta movimiento" @@ -188,32 +216,44 @@ "true": "Mouvement détecté", "false": "Aucun mouvement détecté" }, + "ja": { + "true": "動きを検知", + "false": "動きが検出されません" + }, + "nl": { + "true": "Contact", + "false": "Geen contact" + }, + "pt": { + "true": "Contato", + "false": "Sem contato" + }, + "pt-BR": { + "true": "Contato", + "false": "Sem contato" + }, "ru": { "true": "Обнаружено движение", "false": "Движение не обнаружено" }, - "ja": { - "true": "動きを検知", - "false": "動きが検出されません" - } - }, - "contact_state": { "zh-Hans": { - "true": "接触", - "false": "分离" + "true": "有人", + "false": "无人" }, "zh-Hant": { - "true": "接觸", - "false": "分離" + "true": "有人", + "false": "無人" + } + }, + "contact_state": { + "de": { + "true": "Kontakt", + "false": "Kein Kontakt" }, "en": { "true": "Contact", "false": "No Contact" }, - "de": { - "true": "Kontakt", - "false": "Kein Kontakt" - }, "es": { "true": "Contacto", "false": "Sin contacto" @@ -222,13 +262,33 @@ "true": "Contact", "false": "Pas de contact" }, + "ja": { + "true": "接触", + "false": "非接触" + }, + "nl": { + "true": "Contact", + "false": "Geen contact" + }, + "pt": { + "true": "Contato", + "false": "Sem contato" + }, + "pt-BR": { + "true": "Contato", + "false": "Sem contato" + }, "ru": { "true": "Контакт", "false": "Нет контакта" }, - "ja": { + "zh-Hans": { "true": "接触", - "false": "非接触" + "false": "分离" + }, + "zh-Hant": { + "true": "接觸", + "false": "分離" } } } diff --git a/custom_components/xiaomi_home/miot/specs/multi_lang.json b/custom_components/xiaomi_home/miot/specs/multi_lang.json index ed1fd96..80cb8b8 100644 --- a/custom_components/xiaomi_home/miot/specs/multi_lang.json +++ b/custom_components/xiaomi_home/miot/specs/multi_lang.json @@ -1,5 +1,27 @@ { "urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": { + "de": { + "service:001": "Geräteinformationen", + "service:001:property:003": "Geräte-ID", + "service:001:property:005": "Seriennummer (SN)", + "service:002": "Gateway", + "service:002:event:001": "Netzwerk geändert", + "service:002:event:002": "Netzwerk geändert", + "service:002:property:001": "Zugriffsmethode", + "service:002:property:001:valuelist:000": "Kabelgebunden", + "service:002:property:001:valuelist:001": "5G Drahtlos", + "service:002:property:001:valuelist:002": "2.4G Drahtlos", + "service:002:property:002": "IP-Adresse", + "service:002:property:003": "WiFi-Netzwerkname", + "service:002:property:004": "Aktuelle Zeit", + "service:002:property:005": "DHCP-Server-MAC-Adresse", + "service:003": "Anzeigelampe", + "service:003:property:001": "Schalter", + "service:004": "Virtueller Dienst", + "service:004:action:001": "Virtuelles Ereignis erzeugen", + "service:004:event:001": "Virtuelles Ereignis aufgetreten", + "service:004:property:001": "Ereignisname" + }, "en": { "service:001": "Device Information", "service:001:property:003": "Device ID", @@ -66,50 +88,6 @@ "service:004:event:001": "Événement virtuel survenu", "service:004:property:001": "Nom de l'événement" }, - "ru": { - "service:001": "Информация об устройстве", - "service:001:property:003": "ID устройства", - "service:001:property:005": "Серийный номер (SN)", - "service:002": "Шлюз", - "service:002:event:001": "Сеть изменена", - "service:002:event:002": "Сеть изменена", - "service:002:property:001": "Метод доступа", - "service:002:property:001:valuelist:000": "Проводной", - "service:002:property:001:valuelist:001": "5G Беспроводной", - "service:002:property:001:valuelist:002": "2.4G Беспроводной", - "service:002:property:002": "IP Адрес", - "service:002:property:003": "Название WiFi сети", - "service:002:property:004": "Текущее время", - "service:002:property:005": "MAC адрес DHCP сервера", - "service:003": "Световой индикатор", - "service:003:property:001": "Переключатель", - "service:004": "Виртуальная служба", - "service:004:action:001": "Создать виртуальное событие", - "service:004:event:001": "Произошло виртуальное событие", - "service:004:property:001": "Название события" - }, - "de": { - "service:001": "Geräteinformationen", - "service:001:property:003": "Geräte-ID", - "service:001:property:005": "Seriennummer (SN)", - "service:002": "Gateway", - "service:002:event:001": "Netzwerk geändert", - "service:002:event:002": "Netzwerk geändert", - "service:002:property:001": "Zugriffsmethode", - "service:002:property:001:valuelist:000": "Kabelgebunden", - "service:002:property:001:valuelist:001": "5G Drahtlos", - "service:002:property:001:valuelist:002": "2.4G Drahtlos", - "service:002:property:002": "IP-Adresse", - "service:002:property:003": "WiFi-Netzwerkname", - "service:002:property:004": "Aktuelle Zeit", - "service:002:property:005": "DHCP-Server-MAC-Adresse", - "service:003": "Anzeigelampe", - "service:003:property:001": "Schalter", - "service:004": "Virtueller Dienst", - "service:004:action:001": "Virtuelles Ereignis erzeugen", - "service:004:event:001": "Virtuelles Ereignis aufgetreten", - "service:004:property:001": "Ereignisname" - }, "ja": { "service:001": "デバイス情報", "service:001:property:003": "デバイスID", @@ -132,6 +110,28 @@ "service:004:event:001": "バーチャルイベントが発生しました", "service:004:property:001": "イベント名" }, + "ru": { + "service:001": "Информация об устройстве", + "service:001:property:003": "ID устройства", + "service:001:property:005": "Серийный номер (SN)", + "service:002": "Шлюз", + "service:002:event:001": "Сеть изменена", + "service:002:event:002": "Сеть изменена", + "service:002:property:001": "Метод доступа", + "service:002:property:001:valuelist:000": "Проводной", + "service:002:property:001:valuelist:001": "5G Беспроводной", + "service:002:property:001:valuelist:002": "2.4G Беспроводной", + "service:002:property:002": "IP Адрес", + "service:002:property:003": "Название WiFi сети", + "service:002:property:004": "Текущее время", + "service:002:property:005": "MAC адрес DHCP сервера", + "service:003": "Световой индикатор", + "service:003:property:001": "Переключатель", + "service:004": "Виртуальная служба", + "service:004:action:001": "Создать виртуальное событие", + "service:004:event:001": "Произошло виртуальное событие", + "service:004:property:001": "Название события" + }, "zh-Hant": { "service:001": "設備信息", "service:001:property:003": "設備ID", diff --git a/custom_components/xiaomi_home/miot/specs/spec_filter.json b/custom_components/xiaomi_home/miot/specs/spec_filter.json index 0a72547..5cea69f 100644 --- a/custom_components/xiaomi_home/miot/specs/spec_filter.json +++ b/custom_components/xiaomi_home/miot/specs/spec_filter.json @@ -1,26 +1,22 @@ { - "urn:miot-spec-v2:device:health-pot:0000A051:chunmi-a1": { + "urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4": { + "properties": [ + "9.*", + "13.*", + "15.*" + ], "services": [ - "5" + "10" ] }, "urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01": { + "properties": [ + "5.1" + ], "services": [ "4", "7", "8" - ], - "properties": [ - "5.1" - ] - }, - "urn:miot-spec-v2:device:light:0000A001:philips-strip3": { - "services": [ - "1", - "3" - ], - "properties": [ - "2.2" ] }, "urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": { @@ -28,12 +24,20 @@ "2.1" ] }, - "urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1": { + "urn:miot-spec-v2:device:health-pot:0000A051:chunmi-a1": { "services": [ - "1", "5" ] }, + "urn:miot-spec-v2:device:light:0000A001:philips-strip3": { + "properties": [ + "2.2" + ], + "services": [ + "1", + "3" + ] + }, "urn:miot-spec-v2:device:light:0000A001:yeelink-color2": { "properties": [ "3.*", @@ -50,14 +54,10 @@ "3" ] }, - "urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4": { + "urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1": { "services": [ - "10" - ], - "properties": [ - "9.*", - "13.*", - "15.*" + "1", + "5" ] } } \ No newline at end of file diff --git a/test/check_rule_format.py b/test/check_rule_format.py index 48d602e..3c20afa 100644 --- a/test/check_rule_format.py +++ b/test/check_rule_format.py @@ -6,9 +6,20 @@ import pytest import yaml -SOURCE_PATH: str = path.dirname(path.abspath(__file__)) -TRANS_RELATIVE_PATH: str = '../custom_components/xiaomi_home/translations' -MIOT_I18N_RELATIVE_PATH: str = '../custom_components/xiaomi_home/miot/i18n' +ROOT_PATH: str = path.dirname(path.abspath(__file__)) +TRANS_RELATIVE_PATH: str = path.join( + ROOT_PATH, '../custom_components/xiaomi_home/translations') +MIOT_I18N_RELATIVE_PATH: str = path.join( + ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n') +SPEC_BOOL_TRANS_FILE = path.join( + ROOT_PATH, + '../custom_components/xiaomi_home/miot/specs/bool_trans.json') +SPEC_MULTI_LANG_FILE = path.join( + ROOT_PATH, + '../custom_components/xiaomi_home/miot/specs/multi_lang.json') +SPEC_FILTER_FILE = path.join( + ROOT_PATH, + '../custom_components/xiaomi_home/miot/specs/spec_filter.json') def load_json_file(file_path: str) -> Optional[dict]: @@ -23,6 +34,11 @@ def load_json_file(file_path: str) -> Optional[dict]: return None +def save_json_file(file_path: str, data: dict) -> None: + with open(file_path, 'w', encoding='utf-8') as file: + json.dump(data, file, ensure_ascii=False, indent=4) + + def load_yaml_file(file_path: str) -> Optional[dict]: try: with open(file_path, 'r', encoding='utf-8') as file: @@ -98,6 +114,16 @@ def bool_trans(d: dict) -> bool: return False if not nested_3_dict_str_str(d['translate']): return False + default_trans: dict = d['translate'].pop('default') + if not default_trans: + print('default trans is empty') + return False + default_keys: set[str] = set(default_trans.keys()) + for key, trans in d['translate'].items(): + trans_keys: set[str] = set(trans.keys()) + if set(trans.keys()) != default_keys: + print('bool trans inconsistent', key, default_keys, trans_keys) + return False return True @@ -125,54 +151,69 @@ def compare_dict_structure(dict1: dict, dict2: dict) -> bool: return True +def sort_bool_trans(file_path: str): + trans_data: dict = load_json_file(file_path=file_path) + trans_data['data'] = dict(sorted(trans_data['data'].items())) + for key, trans in trans_data['translate'].items(): + trans_data['translate'][key] = dict(sorted(trans.items())) + return trans_data + + +def sort_multi_lang(file_path: str): + multi_lang: dict = load_json_file(file_path=file_path) + multi_lang = dict(sorted(multi_lang.items())) + for urn, trans in multi_lang.items(): + multi_lang[urn] = dict(sorted(trans.items())) + for lang, spec in multi_lang[urn].items(): + multi_lang[urn][lang] = dict(sorted(spec.items())) + return multi_lang + + +def sort_spec_filter(file_path: str): + filter_data: dict = load_json_file(file_path=file_path) + filter_data = dict(sorted(filter_data.items())) + for urn, spec in filter_data.items(): + filter_data[urn] = dict(sorted(spec.items())) + return filter_data + + @pytest.mark.github def test_bool_trans(): - data: dict = load_json_file( - path.join( - SOURCE_PATH, - '../custom_components/xiaomi_home/miot/specs/bool_trans.json')) - assert data - assert bool_trans(data) + data: dict = load_json_file(SPEC_BOOL_TRANS_FILE) + assert data, f'load {SPEC_BOOL_TRANS_FILE} failed' + assert bool_trans(data), f'{SPEC_BOOL_TRANS_FILE} format error' @pytest.mark.github def test_spec_filter(): - data: dict = load_json_file( - path.join( - SOURCE_PATH, - '../custom_components/xiaomi_home/miot/specs/spec_filter.json')) - assert data - assert spec_filter(data) + data: dict = load_json_file(SPEC_FILTER_FILE) + assert data, f'load {SPEC_FILTER_FILE} failed' + assert spec_filter(data), f'{SPEC_FILTER_FILE} format error' @pytest.mark.github def test_multi_lang(): - data: dict = load_json_file( - path.join( - SOURCE_PATH, - '../custom_components/xiaomi_home/miot/specs/multi_lang.json')) - assert data - assert nested_3_dict_str_str(data) + data: dict = load_json_file(SPEC_MULTI_LANG_FILE) + assert data, f'load {SPEC_MULTI_LANG_FILE} failed' + assert nested_3_dict_str_str(data), f'{SPEC_MULTI_LANG_FILE} format error' @pytest.mark.github def test_miot_i18n(): - i18n_path: str = path.join(SOURCE_PATH, MIOT_I18N_RELATIVE_PATH) - for file_name in listdir(i18n_path): - file_path: str = path.join(i18n_path, file_name) + for file_name in listdir(MIOT_I18N_RELATIVE_PATH): + file_path: str = path.join(MIOT_I18N_RELATIVE_PATH, file_name) data: dict = load_json_file(file_path) - assert data - assert nested_3_dict_str_str(data) + assert data, f'load {file_path} failed' + assert nested_3_dict_str_str(data), f'{file_path} format error' @pytest.mark.github def test_translations(): - i18n_path: str = path.join(SOURCE_PATH, TRANS_RELATIVE_PATH) - for file_name in listdir(i18n_path): - file_path: str = path.join(i18n_path, file_name) + for file_name in listdir(TRANS_RELATIVE_PATH): + file_path: str = path.join(TRANS_RELATIVE_PATH, file_name) data: dict = load_json_file(file_path) - assert data - assert dict_str_dict(data) + assert data, f'load {file_path} failed' + assert dict_str_dict(data), f'{file_path} format error' @pytest.mark.github @@ -181,30 +222,72 @@ def test_miot_lang_integrity(): from miot.const import INTEGRATION_LANGUAGES integration_lang_list: list[str] = [ f'{key}.json' for key in list(INTEGRATION_LANGUAGES.keys())] - translations_names: set[str] = set(listdir( - path.join(SOURCE_PATH, TRANS_RELATIVE_PATH))) + translations_names: set[str] = set(listdir(TRANS_RELATIVE_PATH)) assert len(translations_names) == len(integration_lang_list) assert translations_names == set(integration_lang_list) - i18n_names: set[str] = set(listdir( - path.join(SOURCE_PATH, MIOT_I18N_RELATIVE_PATH))) + i18n_names: set[str] = set(listdir(MIOT_I18N_RELATIVE_PATH)) assert len(i18n_names) == len(translations_names) assert i18n_names == translations_names + bool_trans_data: set[str] = load_json_file(SPEC_BOOL_TRANS_FILE) + bool_trans_names: set[str] = set( + bool_trans_data['translate']['default'].keys()) + assert len(bool_trans_names) == len(translations_names) # Check translation files structure default_dict: dict = load_json_file( - path.join(SOURCE_PATH, TRANS_RELATIVE_PATH, integration_lang_list[0])) + path.join(TRANS_RELATIVE_PATH, integration_lang_list[0])) for name in list(integration_lang_list)[1:]: compare_dict: dict = load_json_file( - path.join(SOURCE_PATH, TRANS_RELATIVE_PATH, name)) + path.join(TRANS_RELATIVE_PATH, name)) if not compare_dict_structure(default_dict, compare_dict): print('compare_dict_structure failed /translations, ', name) assert False # Check i18n files structure default_dict = load_json_file( - path.join( - SOURCE_PATH, MIOT_I18N_RELATIVE_PATH, integration_lang_list[0])) + path.join(MIOT_I18N_RELATIVE_PATH, integration_lang_list[0])) for name in list(integration_lang_list)[1:]: compare_dict: dict = load_json_file( - path.join(SOURCE_PATH, MIOT_I18N_RELATIVE_PATH, name)) + path.join(MIOT_I18N_RELATIVE_PATH, name)) if not compare_dict_structure(default_dict, compare_dict): print('compare_dict_structure failed /miot/i18n, ', name) assert False + + +@pytest.mark.github +def test_miot_data_sort(): + # pylint: disable=import-outside-toplevel + from miot.const import INTEGRATION_LANGUAGES + sort_langs: dict = dict(sorted(INTEGRATION_LANGUAGES.items())) + assert list(INTEGRATION_LANGUAGES.keys()) == list(sort_langs.keys()), ( + 'INTEGRATION_LANGUAGES not sorted, correct order\r\n' + f'{list(sort_langs.keys())}') + assert json.dumps( + load_json_file(file_path=SPEC_BOOL_TRANS_FILE)) == json.dumps( + sort_bool_trans(file_path=SPEC_BOOL_TRANS_FILE)), ( + f'{SPEC_BOOL_TRANS_FILE} not sorted, goto project root path' + ' and run the following command sorting, ', + 'pytest -s -v -m update ./test/check_rule_format.py') + assert json.dumps( + load_json_file(file_path=SPEC_MULTI_LANG_FILE)) == json.dumps( + sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE)), ( + f'{SPEC_MULTI_LANG_FILE} not sorted, goto project root path' + ' and run the following command sorting, ', + 'pytest -s -v -m update ./test/check_rule_format.py') + assert json.dumps( + load_json_file(file_path=SPEC_FILTER_FILE)) == json.dumps( + sort_spec_filter(file_path=SPEC_FILTER_FILE)), ( + f'{SPEC_FILTER_FILE} not sorted, goto project root path' + ' and run the following command sorting, ', + 'pytest -s -v -m update ./test/check_rule_format.py') + + +@pytest.mark.update +def test_sort_spec_data(): + sort_data: dict = sort_bool_trans(file_path=SPEC_BOOL_TRANS_FILE) + save_json_file(file_path=SPEC_BOOL_TRANS_FILE, data=sort_data) + print(SPEC_BOOL_TRANS_FILE, 'formatted.') + sort_data = sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE) + save_json_file(file_path=SPEC_MULTI_LANG_FILE, data=sort_data) + print(SPEC_MULTI_LANG_FILE, 'formatted.') + sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE) + save_json_file(file_path=SPEC_FILTER_FILE, data=sort_data) + print(SPEC_FILTER_FILE, 'formatted.') diff --git a/test/pytest.ini b/test/pytest.ini index 098fdc0..7f37f33 100644 --- a/test/pytest.ini +++ b/test/pytest.ini @@ -1,3 +1,4 @@ [pytest] markers: - github: tests for github actions \ No newline at end of file + github: tests for github actions + update: update or re-sort config file \ No newline at end of file From 5f5b3feea55422ef64004841cc41e8e2518dea4f Mon Sep 17 00:00:00 2001 From: Alireza <58640233+SudoACoder@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:26:51 +0330 Subject: [PATCH 34/44] Fix: Replace inconsistent logging.error with _LOGGER.error (#262) --- custom_components/xiaomi_home/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/xiaomi_home/notify.py b/custom_components/xiaomi_home/notify.py index 42d0d17..f0b46ac 100644 --- a/custom_components/xiaomi_home/notify.py +++ b/custom_components/xiaomi_home/notify.py @@ -119,7 +119,7 @@ async def async_send_message( in_value: list[dict] = [] for index, prop in enumerate(self.spec.in_): if type(in_list[index]).__name__ != prop.format_: - logging.error( + _LOGGER.error( 'action exec failed, %s(%s), invalid params item, ' 'which item(%s) in the list must be %s, %s', self.name, self.entity_id, prop.description_trans, From 640ac25d9cde8c3fff2b7e1f71c10968a2073d98 Mon Sep 17 00:00:00 2001 From: Li Shuzhen Date: Fri, 20 Dec 2024 15:03:34 +0800 Subject: [PATCH 35/44] chore: remove default label (#276) --- .github/ISSUE_TEMPLATE/bug_report.yaml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index e4ac606..2889010 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,11 +1,9 @@ name: Bug report / 报告问题 description: Create a report to help us improve. / 报告问题以帮助我们改进 -title: "[Bug]: " -labels: ["bug"] body: - type: input attributes: - label: Describe the bug / 描述问题 + label: Describe the Bug / 描述问题 description: | > A clear and concise description of what the bug is. > 清晰且简明地描述问题。 @@ -14,7 +12,7 @@ body: - type: textarea attributes: - label: To Reproduce / 复现步骤 + label: How to Reproduce / 复现步骤 description: | > If applicable, add screenshots to help explain your problem. You can attach images by clicking this area to highlight it and then dragging files in. Steps to reproduce the behavior: > 如有需要,可添加截图以帮助解释问题。点击此区域以高亮显示并拖动截图文件以上传。请详细描述复现步骤: @@ -28,7 +26,7 @@ body: - type: input attributes: - label: Expected behavior / 预期结果 + label: Expected Behavior / 预期结果 description: | > A clear and concise description of what you expected to happen. > 描述预期结果。 @@ -44,7 +42,7 @@ body: - type: input attributes: - label: Home Assistant Core version / Home Assistant Core 版本 + label: Home Assistant Core Version / Home Assistant Core 版本 description: | > [Settings > About](https://my.home-assistant.io/redirect/info) > [设置 > 关于 Home Assistant](https://my.home-assistant.io/redirect/info) @@ -54,7 +52,7 @@ body: - type: input attributes: - label: Home Assistant Operation System version / Home Assistant Operation System 版本 + label: Home Assistant Operation System Version / Home Assistant Operation System 版本 description: | > [Settings > About](https://my.home-assistant.io/redirect/info) > [设置 > 关于 Home Assistant](https://my.home-assistant.io/redirect/info) @@ -64,7 +62,7 @@ body: - type: input attributes: - label: Xiaomi Home integration version / 米家集成版本 + label: Xiaomi Home Integration Version / 米家集成版本 description: | > [Settings > Devices & services > Configured > `Xiaomi Home`](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > [设置 > 设备与服务 > 已配置 > `Xiaomi Home`](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) @@ -74,4 +72,4 @@ body: - type: textarea attributes: - label: Additional context / 其他说明 + label: Additional Context / 其他说明 From a879ae2cdfcba7999de527853782c00509b8623d Mon Sep 17 00:00:00 2001 From: Zeyu Li Date: Fri, 20 Dec 2024 17:34:03 +0800 Subject: [PATCH 36/44] docs: fix README_zh.md about changelog (#270) fix: error messages and incorrent return value which could possibly lead to crash in mint_storage.py --- custom_components/xiaomi_home/miot/miot_storage.py | 8 ++++---- doc/README_zh.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/xiaomi_home/miot/miot_storage.py b/custom_components/xiaomi_home/miot/miot_storage.py index 19f4b4f..dfd6b28 100644 --- a/custom_components/xiaomi_home/miot/miot_storage.py +++ b/custom_components/xiaomi_home/miot/miot_storage.py @@ -129,7 +129,7 @@ def __load( self, full_path: str, type_: type = bytes, with_hash_check: bool = True ) -> Union[bytes, str, dict, list, None]: if not os.path.exists(full_path): - _LOGGER.debug('load error, file not exists, %s', full_path) + _LOGGER.debug('load error, file does not exist, %s', full_path) return None if not os.access(full_path, os.R_OK): _LOGGER.error('load error, file not readable, %s', full_path) @@ -160,7 +160,7 @@ def __load( if type_ in [dict, list]: return json.loads(data_bytes) _LOGGER.error( - 'load error, un-support data type, %s', type_.__name__) + 'load error, unsupported data type, %s', type_.__name__) return None except (OSError, TypeError) as e: _LOGGER.error('load error, %s, %s', e, traceback.format_exc()) @@ -219,8 +219,8 @@ def __save( w_bytes = json.dumps(data).encode('utf-8') else: _LOGGER.error( - 'save error, un-support data type, %s', type_.__name__) - return None + 'save error, unsupported data type, %s', type_.__name__) + return False with open(full_path, 'wb') as w_file: w_file.write(w_bytes) if with_hash: diff --git a/doc/README_zh.md b/doc/README_zh.md index 3b4ead3..c221440 100644 --- a/doc/README_zh.md +++ b/doc/README_zh.md @@ -379,7 +379,7 @@ siid、piid、eiid、aiid、value 均为十进制三位整数。 - [许可证](../LICENSE.md) - 贡献指南: [English](../CONTRIBUTING.md) | [简体中文](./CONTRIBUTING_zh.md) -- [更新日志](./CHANGELOG.md) +- [更新日志](../CHANGELOG.md) - 开发文档: https://developers.home-assistant.io/docs/creating_component_index ## 目录结构 From aacb794e1fe89caaa4da5c22a58d589b6630a275 Mon Sep 17 00:00:00 2001 From: Feng Wang Date: Fri, 20 Dec 2024 17:34:34 +0800 Subject: [PATCH 37/44] feat: Use aiohttp instead of waiting for blocking calls (#227) * Use native async call instead of converting blocking calls * remove nullable declarations * fixs * Fix star expression * fix gather again * remove unused private function * Fix naming conflict * Add the deleted function back. Disable the warning instead. * remove trailing space * handle wrong mime type from cloud * Fix request header * fix missing await --- custom_components/xiaomi_home/manifest.json | 3 +- .../xiaomi_home/miot/miot_cloud.py | 185 +++++++----------- 2 files changed, 77 insertions(+), 111 deletions(-) diff --git a/custom_components/xiaomi_home/manifest.json b/custom_components/xiaomi_home/manifest.json index 406fe9d..5f55d15 100644 --- a/custom_components/xiaomi_home/manifest.json +++ b/custom_components/xiaomi_home/manifest.json @@ -23,7 +23,8 @@ "paho-mqtt<=2.0.0", "numpy", "cryptography", - "psutil" + "psutil", + "aiohttp[speedups]" ], "version": "v0.1.2", "zeroconf": [ diff --git a/custom_components/xiaomi_home/miot/miot_cloud.py b/custom_components/xiaomi_home/miot/miot_cloud.py index 270a66d..dd4c282 100644 --- a/custom_components/xiaomi_home/miot/miot_cloud.py +++ b/custom_components/xiaomi_home/miot/miot_cloud.py @@ -51,10 +51,9 @@ import logging import re import time -from functools import partial from typing import Optional from urllib.parse import urlencode -import requests +import aiohttp # pylint: disable=relative-beyond-top-level from .common import calc_group_id @@ -71,8 +70,9 @@ class MIoTOauthClient: """oauth agent url, default: product env.""" - _main_loop: asyncio.AbstractEventLoop = None - _oauth_host: str = None + _main_loop: asyncio.AbstractEventLoop + _session: aiohttp.ClientSession + _oauth_host: str _client_id: int _redirect_url: str @@ -94,9 +94,10 @@ def __init__( self._oauth_host = DEFAULT_OAUTH2_API_HOST else: self._oauth_host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}' + self._session = aiohttp.ClientSession() - async def __call_async(self, func): - return await self._main_loop.run_in_executor(executor=None, func=func) + def __del__(self): + self._session.close() def set_redirect_url(self, redirect_url: str) -> None: if not isinstance(redirect_url, str) or redirect_url.strip() == '': @@ -140,21 +141,22 @@ def gen_auth_url( return f'{OAUTH2_AUTH_URL}?{encoded_params}' - def _get_token(self, data) -> dict: - http_res = requests.get( + async def __get_token_async(self, data) -> dict: + http_res = await self._session.get( url=f'https://{self._oauth_host}/app/v2/ha/oauth/get_token', params={'data': json.dumps(data)}, headers={'content-type': 'application/x-www-form-urlencoded'}, timeout=MIHOME_HTTP_API_TIMEOUT ) - if http_res.status_code == 401: + if http_res.status == 401: raise MIoTOauthError( 'unauthorized(401)', MIoTErrorCode.CODE_OAUTH_UNAUTHORIZED) - if http_res.status_code != 200: + if http_res.status != 200: raise MIoTOauthError( - f'invalid http status code, {http_res.status_code}') + f'invalid http status code, {http_res.status}') - res_obj = http_res.json() + res_str = await http_res.text() + res_obj = json.loads(res_str) if ( not res_obj or res_obj.get('code', None) != 0 @@ -172,7 +174,7 @@ def _get_token(self, data) -> dict: (res_obj['result'].get('expires_in', 0)*TOKEN_EXPIRES_TS_RATIO)) } - def get_access_token(self, code: str) -> dict: + async def get_access_token_async(self, code: str) -> dict: """get access token by authorization code Args: @@ -184,16 +186,13 @@ def get_access_token(self, code: str) -> dict: if not isinstance(code, str): raise MIoTOauthError('invalid code') - return self._get_token(data={ + return await self.__get_token_async(data={ 'client_id': self._client_id, 'redirect_uri': self._redirect_url, 'code': code, }) - async def get_access_token_async(self, code: str) -> dict: - return await self.__call_async(partial(self.get_access_token, code)) - - def refresh_access_token(self, refresh_token: str) -> dict: + async def refresh_access_token_async(self, refresh_token: str) -> dict: """get access token by refresh token. Args: @@ -205,16 +204,12 @@ def refresh_access_token(self, refresh_token: str) -> dict: if not isinstance(refresh_token, str): raise MIoTOauthError('invalid refresh_token') - return self._get_token(data={ + return await self._get_token_async(data={ 'client_id': self._client_id, 'redirect_uri': self._redirect_url, 'refresh_token': refresh_token, }) - async def refresh_access_token_async(self, refresh_token: str) -> dict: - return await self.__call_async( - partial(self.refresh_access_token, refresh_token)) - class MIoTHttpClient: """MIoT http client.""" @@ -222,6 +217,7 @@ class MIoTHttpClient: GET_PROP_AGGREGATE_INTERVAL: float = 0.2 GET_PROP_MAX_REQ_COUNT = 150 _main_loop: asyncio.AbstractEventLoop + _session: aiohttp.ClientSession _host: str _base_url: str _client_id: str @@ -254,10 +250,10 @@ def __init__( cloud_server=cloud_server, client_id=client_id, access_token=access_token) - async def __call_async(self, func) -> any: - if self._main_loop is None: - raise MIoTHttpError('miot http, un-support async methods') - return await self._main_loop.run_in_executor(executor=None, func=func) + self._session = aiohttp.ClientSession() + + def __del__(self): + self._session.close() def update_http_header( self, cloud_server: Optional[str] = None, @@ -276,36 +272,35 @@ def update_http_header( self._access_token = access_token @property - def __api_session(self) -> requests.Session: - session = requests.Session() - session.headers.update({ + def __api_request_headers(self) -> dict: + return { 'Host': self._host, 'X-Client-BizId': 'haapi', 'Content-Type': 'application/json', 'Authorization': f'Bearer{self._access_token}', 'X-Client-AppId': self._client_id, - }) - return session + } - def mihome_api_get( + # pylint: disable=unused-private-member + async def __mihome_api_get_async( self, url_path: str, params: dict, timeout: int = MIHOME_HTTP_API_TIMEOUT ) -> dict: - http_res = None - with self.__api_session as session: - http_res = session.get( - url=f'{self._base_url}{url_path}', - params=params, - timeout=timeout) - if http_res.status_code == 401: + http_res = await self._session.get( + url=f'{self._base_url}{url_path}', + params=params, + headers=self.__api_request_headers, + timeout=timeout) + if http_res.status == 401: raise MIoTHttpError( 'mihome api get failed, unauthorized(401)', MIoTErrorCode.CODE_HTTP_INVALID_ACCESS_TOKEN) - if http_res.status_code != 200: + if http_res.status != 200: raise MIoTHttpError( - f'mihome api get failed, {http_res.status_code}, ' + f'mihome api get failed, {http_res.status}, ' f'{url_path}, {params}') - res_obj: dict = http_res.json() + res_str = await http_res.text() + res_obj: dict = json.loads(res_str) if res_obj.get('code', None) != 0: raise MIoTHttpError( f'invalid response code, {res_obj.get("code",None)}, ' @@ -315,28 +310,25 @@ def mihome_api_get( self._base_url, url_path, params, res_obj) return res_obj - def mihome_api_post( + async def __mihome_api_post_async( self, url_path: str, data: dict, timeout: int = MIHOME_HTTP_API_TIMEOUT ) -> dict: - encoded_data = None - if data: - encoded_data = json.dumps(data).encode('utf-8') - http_res = None - with self.__api_session as session: - http_res = session.post( - url=f'{self._base_url}{url_path}', - data=encoded_data, - timeout=timeout) - if http_res.status_code == 401: + http_res = await self._session.post( + url=f'{self._base_url}{url_path}', + json=data, + headers=self.__api_request_headers, + timeout=timeout) + if http_res.status == 401: raise MIoTHttpError( 'mihome api get failed, unauthorized(401)', MIoTErrorCode.CODE_HTTP_INVALID_ACCESS_TOKEN) - if http_res.status_code != 200: + if http_res.status != 200: raise MIoTHttpError( - f'mihome api post failed, {http_res.status_code}, ' + f'mihome api post failed, {http_res.status}, ' f'{url_path}, {data}') - res_obj: dict = http_res.json() + res_str = await http_res.text() + res_obj: dict = json.loads(res_str) if res_obj.get('code', None) != 0: raise MIoTHttpError( f'invalid response code, {res_obj.get("code",None)}, ' @@ -346,8 +338,8 @@ def mihome_api_post( self._base_url, url_path, data, res_obj) return res_obj - def get_user_info(self) -> dict: - http_res = requests.get( + 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}, @@ -355,7 +347,8 @@ def get_user_info(self) -> dict: timeout=MIHOME_HTTP_API_TIMEOUT ) - res_obj = http_res.json() + res_str = await http_res.text() + res_obj = json.loads(res_str) if ( not res_obj or res_obj.get('code', None) != 0 @@ -366,14 +359,11 @@ def get_user_info(self) -> dict: return res_obj['data'] - async def get_user_info_async(self) -> dict: - return await self.__call_async(partial(self.get_user_info)) - - def get_central_cert(self, csr: str) -> Optional[str]: + async def get_central_cert_async(self, csr: str) -> Optional[str]: if not isinstance(csr, str): raise MIoTHttpError('invalid params') - res_obj: dict = self.mihome_api_post( + res_obj: dict = await self.__mihome_api_post_async( url_path='/app/v2/ha/oauth/get_central_crt', data={ 'csr': str(base64.b64encode(csr.encode('utf-8')), 'utf-8') @@ -387,11 +377,8 @@ def get_central_cert(self, csr: str) -> Optional[str]: return cert - async def get_central_cert_async(self, csr: str) -> Optional[str]: - return await self.__call_async(partial(self.get_central_cert, csr)) - - def __get_dev_room_page(self, max_id: str = None) -> dict: - res_obj = self.mihome_api_post( + async def __get_dev_room_page_async(self, max_id: str = None) -> dict: + res_obj = await self.__mihome_api_post_async( url_path='/app/v2/homeroom/get_dev_room_page', data={ 'start_id': max_id, @@ -419,7 +406,7 @@ def __get_dev_room_page(self, max_id: str = None) -> dict: res_obj['result'].get('has_more', False) and isinstance(res_obj['result'].get('max_id', None), str) ): - next_list = self.__get_dev_room_page( + next_list = await self.__get_dev_room_page_async( max_id=res_obj['result']['max_id']) for home_id, info in next_list.items(): home_list.setdefault(home_id, {'dids': [], 'room_info': {}}) @@ -432,8 +419,8 @@ def __get_dev_room_page(self, max_id: str = None) -> dict: return home_list - def get_homeinfos(self) -> dict: - res_obj = self.mihome_api_post( + async def get_homeinfos_async(self) -> dict: + res_obj = await self.__mihome_api_post_async( url_path='/app/v2/homeroom/gethome', data={ 'limit': 150, @@ -485,7 +472,7 @@ def get_homeinfos(self) -> dict: res_obj['result'].get('has_more', False) and isinstance(res_obj['result'].get('max_id', None), str) ): - more_list = self.__get_dev_room_page( + more_list = await self.__get_dev_room_page_async( max_id=res_obj['result']['max_id']) for home_id, info in more_list.items(): if home_id not in home_infos['homelist']: @@ -507,16 +494,10 @@ def get_homeinfos(self) -> dict: 'share_home_list': home_infos.get('share_home_list', []) } - async def get_homeinfos_async(self) -> dict: - return await self.__call_async(self.get_homeinfos) - - def get_uid(self) -> str: - return self.get_homeinfos().get('uid', None) - async def get_uid_async(self) -> str: return (await self.get_homeinfos_async()).get('uid', None) - def __get_device_list_page( + async def __get_device_list_page_async( self, dids: list[str], start_did: str = None ) -> dict[str, dict]: req_data: dict = { @@ -527,7 +508,7 @@ def __get_device_list_page( if start_did: req_data['start_did'] = start_did device_infos: dict = {} - res_obj = self.mihome_api_post( + res_obj = await self.__mihome_api_post_async( url_path='/app/v2/home/device_list_page', data=req_data ) @@ -578,7 +559,7 @@ def __get_device_list_page( next_start_did = res_obj.get('next_start_did', None) if res_obj.get('has_more', False) and next_start_did: - device_infos.update(self.__get_device_list_page( + device_infos.update(await self.__get_device_list_page_async( dids=dids, start_did=next_start_did)) return device_infos @@ -587,8 +568,7 @@ async def get_devices_with_dids_async( self, dids: list[str] ) -> dict[str, dict]: results: list[dict[str, dict]] = await asyncio.gather( - *[self.__call_async( - partial(self.__get_device_list_page, dids[index:index+150])) + *[self.__get_device_list_page_async(dids[index:index+150]) for index in range(0, len(dids), 150)]) devices = {} for result in results: @@ -665,12 +645,12 @@ async def get_devices_async( 'devices': devices } - def get_props(self, params: list) -> list: + async def get_props_async(self, params: list) -> list: """ params = [{"did": "xxxx", "siid": 2, "piid": 1}, {"did": "xxxxxx", "siid": 2, "piid": 2}] """ - res_obj = self.mihome_api_post( + res_obj = await self.__mihome_api_post_async( url_path='/app/v2/miotspec/prop/get', data={ 'datasource': 1, @@ -681,11 +661,9 @@ def get_props(self, params: list) -> list: raise MIoTHttpError('invalid response result') return res_obj['result'] - async def get_props_async(self, params: list) -> list: - return await self.__call_async(partial(self.get_props, params)) - def get_prop(self, did: str, siid: int, piid: int) -> any: - results = self.get_props( + 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}]) if not results: return None @@ -711,7 +689,7 @@ async def __get_prop_handler(self) -> bool: if not props_buffer: _LOGGER.error('get prop error, empty request list') return False - results = await self.__call_async(partial(self.get_props, props_buffer)) + results = await self.get_props_async(props_buffer) for result in results: if not all( @@ -747,8 +725,7 @@ async def get_prop_async( self, did: str, siid: int, piid: int, immediately: bool = False ) -> any: if immediately: - return await self.__call_async( - partial(self.get_prop, did, siid, piid)) + return await self.__get_prop_async(did, siid, piid) key: str = f'{did}.{siid}.{piid}' prop_obj = self._get_prop_list.get(key, None) if prop_obj: @@ -766,11 +743,11 @@ async def get_prop_async( return await fut - def set_prop(self, params: list) -> list: + async def set_prop_async(self, params: list) -> list: """ params = [{"did": "xxxx", "siid": 2, "piid": 1, "value": False}] """ - res_obj = self.mihome_api_post( + res_obj = await self.__mihome_api_post_async( url_path='/app/v2/miotspec/prop/set', data={ 'params': params @@ -782,20 +759,14 @@ def set_prop(self, params: list) -> list: return res_obj['result'] - async def set_prop_async(self, params: list) -> list: - """ - params = [{"did": "xxxx", "siid": 2, "piid": 1, "value": False}] - """ - return await self.__call_async(partial(self.set_prop, params)) - - def action( + async def action_async( self, did: str, siid: int, aiid: int, in_list: list[dict] ) -> dict: """ params = {"did": "xxxx", "siid": 2, "aiid": 1, "in": []} """ # NOTICE: Non-standard action param - res_obj = self.mihome_api_post( + res_obj = await self.__mihome_api_post_async( url_path='/app/v2/miotspec/action', data={ 'params': { @@ -810,9 +781,3 @@ def action( 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: - return await self.__call_async( - partial(self.action, did, siid, aiid, in_list)) From 6ce3206b30841db7a6d551a16dedafbf58746f43 Mon Sep 17 00:00:00 2001 From: whoiam <46884440+Jimmo-o@users.noreply.github.com> Date: Fri, 20 Dec 2024 17:35:21 +0800 Subject: [PATCH 38/44] fix: HASS core version requirements (#214) * Update README.md * Update hacs.json * Update README_zh.md --- README.md | 2 +- doc/README_zh.md | 2 +- hacs.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fe3b75c..2268b9d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Xiaomi Home Integration is an integrated component of Home Assistant supported b > Home Assistant version requirement: > -> - Core $\geq$ 2024.11.0 +> - Core $\geq$ 2024.4.4 > - Operating System $\geq$ 13.0 ### Method 1: Git clone from GitHub diff --git a/doc/README_zh.md b/doc/README_zh.md index c221440..7e1a48b 100644 --- a/doc/README_zh.md +++ b/doc/README_zh.md @@ -8,7 +8,7 @@ > Home Assistant 版本要求: > -> - Core $\geq$ 2024.11.0 +> - Core $\geq$ 2024.4.4 > - Operating System $\geq$ 13.0 ### 方法 1:使用 git clone 命令从 GitHub 下载 diff --git a/hacs.json b/hacs.json index 98096d8..366fc8e 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { "name": "Xiaomi Home", - "homeassistant": "2024.11.0", + "homeassistant": "2024.4.4", "hacs": "1.34.0" } From bd3a98b976d3092c194479bf2fcfaaf3f8a0c1af Mon Sep 17 00:00:00 2001 From: Paul Shawn <32349595+topsworld@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:21:43 +0800 Subject: [PATCH 39/44] Fix local ctrl error (#271) * feat: common.py add gen_absolute_path, load_yaml_file * fix: miot_lan add profile devices filter * fix: add lan ctrl profile model list * test: improve lan test * fix: fix pylint redefined-outer-name * feat: update tools to update profile models file * fix: fix pylint waning * fix: update miot lan NETWORK_UNSTABLE_RESUME_TH value --- custom_components/xiaomi_home/miot/common.py | 21 +- .../xiaomi_home/miot/lan/profile_models.yaml | 1320 +++++++++++++++++ .../xiaomi_home/miot/miot_lan.py | 29 +- test/conftest.py | 7 + test/test_lan.py | 43 +- tools/common.py | 44 + tools/update_lan_rule.py | 80 + 7 files changed, 1534 insertions(+), 10 deletions(-) create mode 100644 custom_components/xiaomi_home/miot/lan/profile_models.yaml create mode 100644 tools/common.py create mode 100644 tools/update_lan_rule.py diff --git a/custom_components/xiaomi_home/miot/common.py b/custom_components/xiaomi_home/miot/common.py index 5a5137d..0b03f96 100644 --- a/custom_components/xiaomi_home/miot/common.py +++ b/custom_components/xiaomi_home/miot/common.py @@ -46,10 +46,19 @@ Common utilities. """ import json +from os import path import random from typing import Optional import hashlib from paho.mqtt.client import MQTTMatcher +import yaml + +MIOT_ROOT_PATH: str = path.dirname(path.abspath(__file__)) + + +def gen_absolute_path(relative_path: str) -> str: + """Generate an absolute path.""" + return path.join(MIOT_ROOT_PATH, relative_path) def calc_group_id(uid: str, home_id: str) -> str: @@ -64,6 +73,12 @@ def load_json_file(json_file: str) -> dict: return json.load(f) +def load_yaml_file(yaml_file: str) -> dict: + """Load a YAML file.""" + with open(yaml_file, 'r', encoding='utf-8') as f: + return yaml.load(f, Loader=yaml.FullLoader) + + def randomize_int(value: int, ratio: float) -> int: """Randomize an integer value.""" return int(value * (1 - ratio + random.random()*2*ratio)) @@ -74,12 +89,12 @@ class MIoTMatcher(MQTTMatcher): def iter_all_nodes(self) -> any: """Return an iterator on all nodes with their paths and contents.""" - def rec(node, path): + def rec(node, path_): # pylint: disable=protected-access if node._content: - yield ('/'.join(path), node._content) + yield ('/'.join(path_), node._content) for part, child in node._children.items(): - yield from rec(child, path + [part]) + yield from rec(child, path_ + [part]) return rec(self._root, []) def get(self, topic: str) -> Optional[any]: diff --git a/custom_components/xiaomi_home/miot/lan/profile_models.yaml b/custom_components/xiaomi_home/miot/lan/profile_models.yaml new file mode 100644 index 0000000..5e14086 --- /dev/null +++ b/custom_components/xiaomi_home/miot/lan/profile_models.yaml @@ -0,0 +1,1320 @@ +090615.curtain.mt800w: + ts: 1604589513 +090615.curtain.p01: + ts: 1603355069 +090615.curtain.sidt82: + ts: 1604589553 +090615.switch.switch01: + ts: 1605166385 +090615.switch.switch02: + ts: 1604589655 +090615.switch.switch03: + ts: 1605161171 +090615.switch.xswitch01: + ts: 1603965395 +090615.switch.xswitch02: + ts: 1603967482 +090615.switch.xswitch03: + ts: 1603967572 +1245.airpurifier.dl01: + ts: 1607502661 +17216.massage.ec1266a: + ts: 1615881124 +397.light.hallight: + ts: 1605161883 +666.curtain.em75: + ts: 1605857796 +aden.aircondition.a1: + ts: 1618982254 +aden.aircondition.a2: + ts: 1605167390 +aden.aircondition.a4: + ts: 1605167477 +aice.motor.kzmu3: + ts: 1646394583 +air.fan.ca23ad9: + ts: 1635406920 +airdog.airpurifier.x5: + ts: 1623915265 +airdog.airpurifier.x7: + ts: 1614656371 +airdog.airpurifier.x7sm: + ts: 1614656402 +asp.treadmill.pbj: + ts: 1611217046 +aux.aircondition.v1: + ts: 1626427548 +bdx.i_stove.a1xs: + ts: 1610614201 +bdx.i_stove.a1z: + ts: 1614667065 +bdx.i_stove.c1x: + ts: 1610614303 +bdx.i_stove.c2x: + ts: 1611823486 +bj352.airmonitor.m30: + ts: 1686644541 +bj352.waterpuri.s100cm: + ts: 1615795630 +cgllc.airmonitor.b1: + ts: 1676339912 +cgllc.airmonitor.s1: + ts: 1608189468 +cgllc.clock.cgc1: + ts: 1686644422 +cgllc.clock.dove: + ts: 1619607474 +cgllc.magnet.hodor: + ts: 1724329476 +cgllc.motion.cgpr1: + ts: 1639479505 +cgllc.sensor_ht.cgm1: + ts: 1602667557 +cgllc.sensor_ht.dk2: + ts: 1620724988 +cgllc.sensor_ht.g1: + ts: 1607505446 +cgllc.sensor_ht.qpg1: + ts: 1602667461 +chuangmi.camera.ipc004b: + ts: 1531108800 +chuangmi.camera.ipc007b: + ts: 1531108800 +chuangmi.camera.ipc009: + ts: 1531108800 +chuangmi.camera.ipc010: + ts: 1531108800 +chuangmi.camera.ipc013: + ts: 1531108800 +chuangmi.camera.ipc013d: + ts: 1531108800 +chuangmi.camera.ipc016: + ts: 1531108800 +chuangmi.camera.ipc017: + ts: 1531108800 +chuangmi.camera.ipc019: + ts: 1531108800 +chuangmi.camera.ipc019b: + ts: 1531108800 +chuangmi.camera.ipc019e: + ts: 1531108800 +chuangmi.camera.ipc020: + ts: 1531108800 +chuangmi.camera.ipc021: + ts: 1531108800 +chuangmi.camera.v2: + ts: 1531108800 +chuangmi.camera.v3: + ts: 1531108800 +chuangmi.camera.v4: + ts: 1531108800 +chuangmi.camera.v5: + ts: 1531108800 +chuangmi.camera.v6: + ts: 1531108800 +chuangmi.camera.xiaobai: + ts: 1531108800 +chuangmi.cateye.i023a01: + ts: 1624535092 +chuangmi.cateye.ipc018: + ts: 1632735241 +chuangmi.cateye.ipc508: + ts: 1633677521 +chuangmi.door.hmi515: + ts: 1640334316 +chuangmi.lock.hmi501: + ts: 1614742147 +chuangmi.lock.hmi501b01: + ts: 1614742108 +chuangmi.lock.hmi503a01: + ts: 1614742180 +chuangmi.lock.hmi505a01: + ts: 1614742900 +chuangmi.plug.hmi206: + ts: 1645409193 +chuangmi.plug.hmi208: + ts: 1677652804 +chuangmi.plug.m1: + ts: 1620814339 +chuangmi.plug.m3: + ts: 1637228027 +chuangmi.plug.v1: + ts: 1621925183 +chuangmi.plug.v3: + ts: 1644480255 +chuangmi.radio.v1: + ts: 1531108800 +chuangmi.radio.v2: + ts: 1531108800 +chunmi.cooker.eh1: + ts: 1607339278 +chunmi.cooker.eh402: + ts: 1638506382 +chunmi.cooker.k1pro1: + ts: 1607393855 +chunmi.cooker.normal2: + ts: 1607393898 +chunmi.cooker.normal3: + ts: 1620369915 +chunmi.cooker.normal4: + ts: 1607394370 +chunmi.cooker.normal5: + ts: 1607394381 +chunmi.cooker.normalcd1: + ts: 1607395198 +chunmi.cooker.normalcd2: + ts: 1607395220 +chunmi.cooker.press1: + ts: 1607395268 +chunmi.cooker.press2: + ts: 1607395280 +chunmi.ihcooker.chefnic: + ts: 1604591216 +chunmi.ihcooker.double: + ts: 1609924806 +chunmi.ihcooker.hk1: + ts: 1605167058 +chunmi.ihcooker.tkpro1: + ts: 1614756253 +chunmi.ihcooker.tkv1: + ts: 1615801005 +chunmi.ihcooker.v1: + ts: 1605167116 +chunmi.juicer.a1: + ts: 1624530635 +chunmi.microwave.n20l01: + ts: 1638505282 +chunmi.microwave.n23l01: + ts: 1638505524 +chunmi.oven.steam30lv1: + ts: 1605855556 +chunmi.pre_cooker.eh1: + ts: 1614570773 +cleargrass.sensor_ht.dk1: + ts: 1619344277 +deerma.humidifier.jsq1: + ts: 1683863719 +deerma.humidifier.mjjsq: + ts: 1684727440 +dicook.cooker.wfz4003: + ts: 1614678405 +dmaker.airfresh.a1: + ts: 1715677691 +dmaker.airfresh.t2017: + ts: 1686731233 +dmaker.fan.p5: + ts: 1655793784 +dsm.lock.h3: + ts: 1615283790 +dsm.lock.q3: + ts: 1614741870 +dsm.lock.r5: + ts: 1614741913 +dun.cateye.nknk500: + ts: 1615358692 +duoqin.safe.pbfv01: + ts: 1614740167 +fawad.airrtc.fwd20011: + ts: 1610607149 +fbs.airmonitor.pth02: + ts: 1686644918 +hannto.printer.anise: + ts: 1618989537 +hannto.printer.honey: + ts: 1607504864 +hannto.printer.honey1s: + ts: 1614332725 +hfjh.fishbowl.v1: + ts: 1615278556 +hhcc.plantmonitor.v1: + ts: 1664163526 +hith.foot_bath.q2: + ts: 1531108800 +huohe.lock.m1: + ts: 1635410938 +hutlon.lock.v0001: + ts: 1634799698 +idelan.aircondition.v1: + ts: 1614666973 +idelan.aircondition.v2: + ts: 1626427579 +ihealth.bp.bpm1: + ts: 1608189506 +ihealth.bpm.kd5907: + ts: 1614673307 +ikea.light.led1536g5: + ts: 1605162819 +ikea.light.led1537r6: + ts: 1605162872 +ikea.light.led1545g12: + ts: 1605162937 +ikea.light.led1623g12: + ts: 1605163009 +ikea.light.led1649c5: + ts: 1605163064 +imibar.cooker.mbihr3: + ts: 1624620659 +imou99.camera.tp2: + ts: 1531108800 +isa.camera.df3: + ts: 1531108800 +isa.camera.hl5: + ts: 1531108800 +isa.camera.hlc6: + ts: 1531108800 +isa.camera.isc5: + ts: 1531108800 +isa.camera.isc5c1: + ts: 1621238175 +isa.magnet.dw2hl: + ts: 1638274655 +jiqid.mistory.pro: + ts: 1531108800 +jiqid.mistory.v1: + ts: 1531108800 +jiqid.mistudy.v2: + ts: 1610612349 +jiwu.lock.jwp01: + ts: 1614752632 +jyaiot.cm.ccj01: + ts: 1611824545 +ksmb.treadmill.v1: + ts: 1611211447 +ksmb.treadmill.v2: + ts: 1610606684 +ksmb.walkingpad.v1: + ts: 1621238199 +ksmb.walkingpad.v3: + ts: 1621238214 +kxf321.mop.mo001: + ts: 1638343629 +lcrmcr.safe.20mini: + ts: 1617765416 +lcrmcr.safe.30mk: + ts: 1628215806 +lcrmcr.safe.an35sidz: + ts: 1614741042 +lcrmcr.safe.an35sizw: + ts: 1614741076 +lcrmcr.safe.d60ht: + ts: 1624351108 +lcrmcr.safe.ms30b: + ts: 1606986586 +lcrmcr.safe.ms30mp: + ts: 1604587256 +lcrmcr.safe.ms55kn: + ts: 1606980410 +lcrmcr.safe.ms80b: + ts: 1614740261 +lcrmcr.safe.sd003: + ts: 1637033698 +lcrmcr.safe.x142: + ts: 1634813125 +leshow.fan.ss310: + ts: 1686896880 +leshow.fan.ss320: + ts: 1686896668 +leshow.fan.ss4: + ts: 1606376586 +leshow.heater.bs1: + ts: 1608187309 +leshow.humidifier.is2: + ts: 1604589602 +linp.doorbell.g03: + ts: 1609311251 +linp.remote.k9b: + ts: 1621825941 +linp.remote.k9b1: + ts: 1621825926 +linp.remote.k9b11: + ts: 1621825903 +loock.cateye.v01: + ts: 1627291525 +loock.cateye.v02: + ts: 1661739898 +loock.lock.cc2s: + ts: 1614752275 +loock.lock.cc2xpro: + ts: 1614752365 +loock.lock.fcl112: + ts: 1658997537 +loock.lock.fcp50m: + ts: 1647246522 +loock.lock.fvl109: + ts: 1640252939 +loock.lock.fvl111: + ts: 1646134370 +loock.lock.ojjz1: + ts: 1614741370 +loock.lock.p50: + ts: 1644572168 +loock.lock.pfvl10: + ts: 1630040903 +loock.lock.s30: + ts: 1614740862 +loock.lock.s30v2: + ts: 1614829588 +loock.lock.s50c: + ts: 1649309703 +loock.lock.s50f: + ts: 1639967451 +loock.lock.t1: + ts: 1634542543 +loock.lock.t1pro: + ts: 1634543113 +loock.lock.t2v1: + ts: 1655983879 +loock.lock.v1: + ts: 1618889646 +loock.lock.v14: + ts: 1614741632 +loock.lock.v15: + ts: 1619508939 +loock.lock.v16: + ts: 1621235279 +loock.lock.v3: + ts: 1619341106 +loock.lock.v4: + ts: 1619340970 +loock.lock.v5: + ts: 1614752242 +loock.lock.v6: + ts: 1634796911 +loock.lock.v7: + ts: 1614741195 +loock.lock.v8: + ts: 1619413983 +loock.lock.v9: + ts: 1614741328 +loock.lock.xfvl10: + ts: 1632814256 +loock.safe.v1: + ts: 1619607755 +lumi.acpartner.v1: + ts: 1531108800 +lumi.acpartner.v2: + ts: 1531108800 +lumi.acpartner.v3: + ts: 1531108800 +lumi.airer.acn01: + ts: 1611818317 +lumi.airrtc.tcpco2ecn01: + ts: 1531108800 +lumi.airrtc.tcpecn01: + ts: 1531108800 +lumi.airrtc.tcpecn02: + ts: 1531108800 +lumi.camera.aq1: + ts: 1531108800 +lumi.camera.gwagl01: + ts: 1531108800 +lumi.ctrl_86plug.aq1: + ts: 1627292331 +lumi.ctrl_86plug.v1: + ts: 1627291597 +lumi.ctrl_ln1.aq1: + ts: 1648791623 +lumi.ctrl_ln1.v1: + ts: 1653297884 +lumi.ctrl_ln2.aq1: + ts: 1653292857 +lumi.ctrl_ln2.v1: + ts: 1653299006 +lumi.ctrl_neutral1.v1: + ts: 1653294867 +lumi.ctrl_neutral2.v1: + ts: 1653294492 +lumi.curtain.aq2: + ts: 1605857829 +lumi.curtain.hagl04: + ts: 1615351634 +lumi.curtain.v1: + ts: 1608188918 +lumi.flood.bmcn01: + ts: 1614666824 +lumi.gateway.aqhm01: + ts: 1687160720 +lumi.gateway.aqhm02: + ts: 1687162682 +lumi.gateway.lmuk01: + ts: 1687164111 +lumi.gateway.mieu01: + ts: 1687163976 +lumi.gateway.mihk01: + ts: 1687163477 +lumi.gateway.mitw01: + ts: 1687160954 +lumi.gateway.v1: + ts: 1687162995 +lumi.gateway.v2: + ts: 1687163777 +lumi.gateway.v3: + ts: 1686895771 +lumi.light.aqcn02: + ts: 1620727535 +lumi.light.cwopcn01: + ts: 1605855768 +lumi.light.cwopcn02: + ts: 1605855809 +lumi.light.cwopcn03: + ts: 1605855836 +lumi.lock.acn02: + ts: 1623928631 +lumi.lock.acn03: + ts: 1614752574 +lumi.lock.bacn01: + ts: 1614741699 +lumi.lock.bmcn02: + ts: 1637292825 +lumi.lock.bmcn03: + ts: 1634546176 +lumi.lock.bmcn04: + ts: 1636451160 +lumi.lock.bmcn05: + ts: 1636454200 +lumi.lock.bzacn1: + ts: 1614741727 +lumi.lock.bzacn2: + ts: 1614741815 +lumi.lock.eicn02: + ts: 1639976382 +lumi.lock.mcn007: + ts: 1650446757 +lumi.lock.mcn01: + ts: 1679881881 +lumi.lock.wbmcn1: + ts: 1619422072 +lumi.motion.bmgl01: + ts: 1639983139 +lumi.plug.v1: + ts: 1653299040 +lumi.relay.c2acn01: + ts: 1609310261 +lumi.remote.b186acn01: + ts: 1614154507 +lumi.remote.b186acn02: + ts: 1607395626 +lumi.remote.b1acn01: + ts: 1608794798 +lumi.remote.b286acn01: + ts: 1607397501 +lumi.remote.b286acn02: + ts: 1608794723 +lumi.remote.b286opcn01: + ts: 1614154602 +lumi.remote.b486opcn01: + ts: 1614154660 +lumi.remote.b686opcn01: + ts: 1614154713 +lumi.sensor_86sw1.v1: + ts: 1609311038 +lumi.sensor_86sw2.v1: + ts: 1608795035 +lumi.sensor_ht.v1: + ts: 1621239877 +lumi.sensor_magnet.aq2: + ts: 1641112867 +lumi.sensor_magnet.v2: + ts: 1641113779 +lumi.sensor_motion.aq2: + ts: 1676433994 +lumi.sensor_motion.v2: + ts: 1672818550 +lumi.sensor_natgas.v1: + ts: 1635762582 +lumi.sensor_smoke.mcn02: + ts: 1634635296 +lumi.sensor_smoke.v1: + ts: 1634809938 +lumi.sensor_switch.aq2: + ts: 1615256430 +lumi.sensor_switch.aq3: + ts: 1607399487 +lumi.sensor_switch.v2: + ts: 1609310683 +lumi.sensor_wleak.aq1: + ts: 1614669352 +lumi.switch.b1lacn02: + ts: 1653297814 +lumi.switch.b1nacn02: + ts: 1653297756 +lumi.switch.b2lacn02: + ts: 1653296711 +lumi.switch.b2nacn02: + ts: 1655201416 +lumi.switch.l3acn3: + ts: 1653296585 +lumi.switch.n3acn3: + ts: 1653294817 +lumi.vibration.aq1: + ts: 1614156721 +lumi.weather.v1: + ts: 1621239934 +madv.alarm.winlock1: + ts: 1611215780 +madv.cateye.dlowl: + ts: 1632714747 +madv.cateye.dlowlplus: + ts: 1615876742 +madv.cateye.dlowlse: + ts: 1607409634 +madv.cateye.dlowlse2: + ts: 1615876830 +madv.cateye.miowl: + ts: 1632714912 +madv.cateye.miowlv2: + ts: 1614152626 +madv.cateye.miowlv2l: + ts: 1615450328 +miaomiaoce.clock.ht02: + ts: 1620728504 +miaomiaoce.sensor_ht.h1: + ts: 1623929417 +miaomiaoce.sensor_ht.t1: + ts: 1616057242 +miaomiaoce.sensor_ht.t2: + ts: 1636603553 +miir.aircondition.ir01: + ts: 1531108800 +miir.aircondition.ir02: + ts: 1531108800 +miir.fan.ir01: + ts: 1531108800 +miir.light.ir01: + ts: 1531108800 +miir.projector.ir01: + ts: 1531108800 +miir.stb.ir01: + ts: 1531108800 +miir.tv.hir01: + ts: 1531108800 +miir.tv.ir01: + ts: 1531108800 +miir.tvbox.ir01: + ts: 1531108800 +mijia.camera.v1: + ts: 1531108800 +mijia.camera.v3: + ts: 1531108800 +minij.washer.v1: + ts: 1611818798 +minij.washer.v10: + ts: 1608792100 +minij.washer.v11: + ts: 1608792159 +minij.washer.v12: + ts: 1614656828 +minij.washer.v14: + ts: 1608792176 +minij.washer.v15: + ts: 1607410326 +minij.washer.v5: + ts: 1622792196 +minij.washer.v8: + ts: 1615777868 +miot.light.plato2: + ts: 1685518142 +miot.light.plato3: + ts: 1675941846 +miot.light.plato4: + ts: 1675941712 +mmgg.feeder.petfeeder: + ts: 1646394400 +mmgg.feeder.snack: + ts: 1607503182 +moyu.washer.s1hm: + ts: 1624620888 +mrbond.airer.m1pro: + ts: 1646393746 +mrbond.airer.m1s: + ts: 1646393874 +msj.f_washer.m1: + ts: 1614914340 +mxiang.cateye.mdb10: + ts: 1616140362 +mxiang.cateye.xmcatt1: + ts: 1616140207 +nwt.derh.wdh318efw1: + ts: 1611822375 +opple.light.bydceiling: + ts: 1608187619 +opple.light.fanlight: + ts: 1608793315 +opple.remote.5pb111: + ts: 1627453753 +opple.remote.5pb112: + ts: 1627453840 +opple.remote.5pb113: + ts: 1636599905 +ows.towel_w.mj1x0: + ts: 1610604939 +philips.light.bceiling1: + ts: 1642048160 +philips.light.bceiling2: + ts: 1642048073 +philips.light.bulb: + ts: 1620814142 +philips.light.candle: + ts: 1620814078 +philips.light.candle2: + ts: 1620814121 +philips.light.cbulb: + ts: 1639039185 +philips.light.ceiling: + ts: 1646394746 +philips.light.downlight: + ts: 1620814088 +philips.light.lnblight1: + ts: 1605857128 +philips.light.lnblight2: + ts: 1623902902 +philips.light.lnlrlight: + ts: 1621845517 +philips.light.lrceiling: + ts: 1642047751 +philips.light.mono1: + ts: 1608790977 +philips.light.moonlight: + ts: 1642582053 +philips.light.nlight: + ts: 1614147878 +philips.light.rwread: + ts: 1603352187 +philips.light.sread1: + ts: 1620814093 +philips.light.sread2: + ts: 1615259714 +philips.light.zyceiling: + ts: 1637228869 +philips.light.zysread: + ts: 1615970237 +philips.light.zystrip: + ts: 1605166337 +phnix.waterheater.sf: + ts: 1616050758 +pwzn.relay.apple: + ts: 1611217196 +pwzn.relay.banana: + ts: 1646647255 +qike.bhf_light.qk201801: + ts: 1608174909 +qmi.powerstrip.v1: + ts: 1621240280 +roborock.sweeper.s5v2: + ts: 1531108800 +roborock.vacuum.a01: + ts: 1685425888 +roborock.vacuum.a08: + ts: 1626231977 +roborock.vacuum.a09: + ts: 1611048152 +roborock.vacuum.a11: + ts: 1615360022 +roborock.vacuum.c1: + ts: 1531108800 +roborock.vacuum.e2: + ts: 1531108800 +roborock.vacuum.m1s: + ts: 1531108800 +roborock.vacuum.p5: + ts: 1611048094 +roborock.vacuum.s5: + ts: 1531108800 +roborock.vacuum.t4: + ts: 1615449261 +roborock.vacuum.t6: + ts: 1619423841 +rockrobo.vacuum.v1: + ts: 1531108800 +roome.bhf_light.yf6002: + ts: 1531108800 +rotai.massage.rt5728: + ts: 1610607000 +rotai.massage.rt5850: + ts: 1611816888 +rotai.massage.rt5850s: + ts: 1616727205 +rotai.massage.rt5863: + ts: 1611827937 +rotai.massage.rt5870: + ts: 1632376570 +scishare.coffee.s1102: + ts: 1611824402 +shuii.humidifier.jsq002: + ts: 1606376290 +skyrc.pet_waterer.fre1: + ts: 1608186812 +smith.waterheater.cxea1: + ts: 1611826349 +smith.waterheater.cxeb1: + ts: 1611826388 +smith.waterpuri.jnt600: + ts: 1531108800 +soocare.toothbrush.m1s: + ts: 1610611310 +soocare.toothbrush.t501: + ts: 1672192586 +sxds.pillow.pillow02: + ts: 1611222235 +syniot.curtain.syc1: + ts: 1608794071 +tinymu.toilet.ailid: + ts: 1615292165 +tinymu.toiletlid.v1: + ts: 1608791137 +tokit.cooker.press1: + ts: 1614842943 +tokit.cooker.tk20l01: + ts: 1646646903 +tokit.cooker.tk4001: + ts: 1639651673 +tokit.ihcooker.tkpro1: + ts: 1624533824 +tokit.ihcooker.tkv1: + ts: 1614667669 +tokit.oven.tk12l01: + ts: 1616729526 +tokit.oven.tk32pro1: + ts: 1617002408 +tokit.pre_cooker.tkih1: + ts: 1607410832 +viomi.aircondition.v10: + ts: 1606375041 +viomi.aircondition.v21: + ts: 1610608182 +viomi.aircondition.v22: + ts: 1610608198 +viomi.aircondition.v23: + ts: 1610608214 +viomi.aircondition.v24: + ts: 1610608242 +viomi.aircondition.v25: + ts: 1610608276 +viomi.aircondition.v26: + ts: 1609924120 +viomi.aircondition.v6: + ts: 1608188576 +viomi.aircondition.v7: + ts: 1608794437 +viomi.aircondition.v8: + ts: 1606376969 +viomi.aircondition.v9: + ts: 1608794507 +viomi.bhf_light.v1: + ts: 1614308454 +viomi.cooker.v1: + ts: 1607410870 +viomi.cooker.v2: + ts: 1607410911 +viomi.curtain.v1: + ts: 1658732587 +viomi.dishwasher.m01: + ts: 1610605899 +viomi.dishwasher.m02: + ts: 1610605964 +viomi.dishwasher.v01: + ts: 1614307805 +viomi.dishwasher.v03: + ts: 1614308206 +viomi.dishwasher.v05: + ts: 1610606831 +viomi.fridge.m1: + ts: 1614667789 +viomi.fridge.p1: + ts: 1614655704 +viomi.fridge.u1: + ts: 1614667927 +viomi.fridge.u12: + ts: 1614666058 +viomi.fridge.u13: + ts: 1614667152 +viomi.fridge.u15: + ts: 1607505693 +viomi.fridge.u18: + ts: 1614655755 +viomi.fridge.u2: + ts: 1531108800 +viomi.fridge.u24: + ts: 1614667214 +viomi.fridge.u4: + ts: 1614667295 +viomi.fridge.u6: + ts: 1614667319 +viomi.fridge.u7: + ts: 1614667341 +viomi.fridge.u8: + ts: 1624589504 +viomi.fridge.v3: + ts: 1614667699 +viomi.fridge.w1: + ts: 1607505903 +viomi.fridge.w2: + ts: 1614655838 +viomi.fridge.x11: + ts: 1614655870 +viomi.fridge.x12: + ts: 1614656914 +viomi.fridge.x4: + ts: 1614223422 +viomi.fridge.x7: + ts: 1637305932 +viomi.health_pot.v1: + ts: 1607502489 +viomi.hood.a10: + ts: 1611823231 +viomi.hood.a9: + ts: 1611823172 +viomi.hood.c1: + ts: 1611220032 +viomi.hood.c2: + ts: 1611823145 +viomi.hood.h1: + ts: 1610612690 +viomi.hood.h3: + ts: 1611823101 +viomi.hood.h4: + ts: 1611823078 +viomi.hood.v1: + ts: 1684390912 +viomi.i_stove.v1: + ts: 1611825720 +viomi.i_stove.v3: + ts: 1606372593 +viomi.juicer.v1: + ts: 1619688553 +viomi.juicer.v2: + ts: 1607504511 +viomi.lock.lbt14a: + ts: 1617940506 +viomi.lock.lbt41e: + ts: 1633750723 +viomi.lock.lbt48a: + ts: 1648788518 +viomi.lock.lbt51a: + ts: 1648536993 +viomi.lock.link1: + ts: 1634546007 +viomi.lock.link2: + ts: 1614740735 +viomi.lock.link2p: + ts: 1626166712 +viomi.lock.link2v: + ts: 1626166615 +viomi.lock.link3: + ts: 1639967286 +viomi.lock.link4s: + ts: 1681980137 +viomi.oven.so1: + ts: 1621243328 +viomi.oven.so2: + ts: 1611816582 +viomi.steriliser.v1: + ts: 1607503139 +viomi.vacuum.v10: + ts: 1619171223 +viomi.vacuum.v3: + ts: 1619171356 +viomi.vacuum.v31: + ts: 1624348346 +viomi.vacuum.v6: + ts: 1620720046 +viomi.vacuum.v7: + ts: 1618969602 +viomi.vacuum.v9: + ts: 1626232015 +viomi.washer.s1: + ts: 1616644328 +viomi.washer.u1: + ts: 1648122511 +viomi.washer.u2: + ts: 1632886871 +viomi.washer.u3: + ts: 1616644429 +viomi.washer.u4: + ts: 1616644467 +viomi.washer.u5: + ts: 1616647921 +viomi.washer.v10: + ts: 1616647971 +viomi.washer.v11: + ts: 1616644507 +viomi.washer.v4: + ts: 1616644547 +viomi.washer.v5: + ts: 1616407442 +viomi.washer.v6: + ts: 1616644619 +viomi.washer.v7: + ts: 1616644253 +viomi.washer.v8: + ts: 1616644309 +viomi.waterheater.e1: + ts: 1615343927 +viomi.waterheater.e3: + ts: 1615344258 +viomi.waterheater.e4: + ts: 1615345547 +viomi.waterheater.e7: + ts: 1604589755 +viomi.waterheater.e8: + ts: 1606982993 +viomi.waterheater.u1: + ts: 1616648390 +viomi.waterheater.u10: + ts: 1606984271 +viomi.waterheater.u11: + ts: 1606984299 +viomi.waterheater.u12: + ts: 1606376892 +viomi.waterheater.u15: + ts: 1646394494 +viomi.waterheater.u16: + ts: 1637304955 +viomi.waterheater.u3: + ts: 1620811497 +viomi.waterheater.u4: + ts: 1615864332 +viomi.waterheater.u6: + ts: 1615442495 +viomi.waterheater.u7: + ts: 1605857988 +viomi.waterheater.u8: + ts: 1615864849 +xiaomi.aircondition.ma1: + ts: 1721628903 +xiaomi.aircondition.ma2: + ts: 1721181139 +xiaomi.aircondition.ma4: + ts: 1726230966 +xiaomi.aircondition.ma5: + ts: 1721629118 +xiaomi.aircondition.ma6: + ts: 1721629272 +xiaomi.aircondition.ma9: + ts: 1721629362 +xiaomi.wifispeaker.l04m: + ts: 1658817956 +xiaomi.wifispeaker.l06a: + ts: 1672731009 +xiaomi.wifispeaker.l09a: + ts: 1626336048 +xiaomi.wifispeaker.l7a: + ts: 1637306490 +xiaomi.wifispeaker.lx01: + ts: 1637306716 +xiaomi.wifispeaker.lx04: + ts: 1669695484 +xiaomi.wifispeaker.lx05: + ts: 1672299502 +xiaomi.wifispeaker.lx06: + ts: 1672299546 +xiaomi.wifispeaker.lx5a: + ts: 1672299577 +xiaomi.wifispeaker.s12: + ts: 1672299594 +xiaomi.wifispeaker.x08a: + ts: 1672818945 +xiaomi.wifispeaker.x08c: + ts: 1658819390 +xiaovv.camera.lamp: + ts: 1531108800 +xiaovv.camera.ptz: + ts: 1531108800 +xiaovv.camera.xva3: + ts: 1531108800 +xiaovv.camera.xvb4: + ts: 1531108800 +xiaovv.camera.xvd5: + ts: 1531108800 +xiaovv.camera.xvsnowman: + ts: 1531108800 +xjx.toilet.pro: + ts: 1615965466 +xjx.toilet.pure: + ts: 1615969114 +xjx.toilet.relax: + ts: 1615968257 +xjx.toilet.zero: + ts: 1618302169 +ydhome.cateye.pr1: + ts: 1615360329 +ydhome.lock.c1p: + ts: 1614740027 +ydhome.lock.m2p: + ts: 1620728837 +ydhome.lock.m2silver: + ts: 1614743037 +ydzl.waterpuri.t1: + ts: 1609926143 +yeelink.bhf_light.v1: + ts: 1608790026 +yeelink.bhf_light.v2: + ts: 1608790085 +yeelink.bhf_light.v3: + ts: 1608790102 +yeelink.bhf_light.v5: + ts: 1601292562 +yeelink.light.bslamp1: + ts: 1703120679 +yeelink.light.bslamp2: + ts: 1703120782 +yeelink.light.bslamp3: + ts: 1646394180 +yeelink.light.ceil27: + ts: 1646394323 +yeelink.light.ceiling1: + ts: 1626343431 +yeelink.light.ceiling10: + ts: 1646393927 +yeelink.light.ceiling11: + ts: 1626341443 +yeelink.light.ceiling12: + ts: 1646122053 +yeelink.light.ceiling13: + ts: 1626341533 +yeelink.light.ceiling14: + ts: 1626341941 +yeelink.light.ceiling15: + ts: 1626342080 +yeelink.light.ceiling16: + ts: 1626342123 +yeelink.light.ceiling17: + ts: 1626342230 +yeelink.light.ceiling18: + ts: 1626342296 +yeelink.light.ceiling19: + ts: 1626342431 +yeelink.light.ceiling2: + ts: 1637308139 +yeelink.light.ceiling20: + ts: 1626339402 +yeelink.light.ceiling21: + ts: 1703121290 +yeelink.light.ceiling22: + ts: 1703121209 +yeelink.light.ceiling23: + ts: 1703120912 +yeelink.light.ceiling24: + ts: 1626343504 +yeelink.light.ceiling3: + ts: 1626343459 +yeelink.light.ceiling4: + ts: 1646393658 +yeelink.light.ceiling5: + ts: 1626342461 +yeelink.light.ceiling6: + ts: 1626343314 +yeelink.light.ceiling7: + ts: 1626343090 +yeelink.light.ceiling8: + ts: 1626343130 +yeelink.light.ceiling9: + ts: 1626343286 +yeelink.light.color1: + ts: 1626343469 +yeelink.light.color2: + ts: 1626343174 +yeelink.light.color3: + ts: 1626166445 +yeelink.light.color4: + ts: 1626343343 +yeelink.light.color5: + ts: 1626227410 +yeelink.light.color6: + ts: 1637309445 +yeelink.light.color7: + ts: 1626227373 +yeelink.light.color8: + ts: 1626343365 +yeelink.light.ct2: + ts: 1626343390 +yeelink.light.lamp1: + ts: 1626342474 +yeelink.light.lamp10: + ts: 1646122763 +yeelink.light.lamp2: + ts: 1638589912 +yeelink.light.lamp3: + ts: 1626343245 +yeelink.light.lamp4: + ts: 1629022996 +yeelink.light.lamp5: + ts: 1626343399 +yeelink.light.lamp7: + ts: 1626418317 +yeelink.light.lamp9: + ts: 1626343409 +yeelink.light.mono1: + ts: 1626343418 +yeelink.light.mono4: + ts: 1626343373 +yeelink.light.mono5: + ts: 1626343352 +yeelink.light.nl1: + ts: 1639479487 +yeelink.light.panel1: + ts: 1626343379 +yeelink.light.panel3: + ts: 1626343334 +yeelink.light.strip1: + ts: 1626343486 +yeelink.light.strip2: + ts: 1732605062 +yeelink.light.strip4: + ts: 1626342354 +yeelink.light.strip6: + ts: 1726730787 +yeelink.mirror.bm1: + ts: 1607504924 +yeelink.remote.remote: + ts: 1608186464 +yeelink.ven_fan.vf1: + ts: 1621237766 +yeelink.wifispeaker.v1: + ts: 1611818727 +yilai.light.ceiling1: + ts: 1603354689 +yilai.light.ceiling2: + ts: 1623911828 +yilai.light.ceiling3: + ts: 1606986226 +yuemee.airmonitor.mhfd1: + ts: 1619608729 +yunlu.door.sd2101: + ts: 1656661489 +yunlu.door.sd2103: + ts: 1657762233 +yunlu.door.sd2104: + ts: 1658317727 +yunmi.kettle.r1: + ts: 1614648943 +yunmi.kettle.r2: + ts: 1606372087 +yunmi.kettle.r3: + ts: 1637309534 +yunmi.plmachine.mg2: + ts: 1611833658 +yunmi.waterpuri.c5: + ts: 1604588322 +yunmi.waterpuri.lx11: + ts: 1604588450 +yunmi.waterpuri.lx12: + ts: 1722239438 +yunmi.waterpuri.lx2: + ts: 1614155130 +yunmi.waterpuri.lx3: + ts: 1606371237 +yunmi.waterpuri.lx4: + ts: 1606978913 +yunmi.waterpuri.lx5: + ts: 1603963118 +yunmi.waterpuri.lx6: + ts: 1606371311 +yunmi.waterpuri.lx7: + ts: 1603963241 +yunmi.waterpuri.lx8: + ts: 1603963353 +yunmi.waterpuri.lx9: + ts: 1607505784 +yunmi.waterpuri.s3: + ts: 1614303228 +yunmi.waterpuri.s4: + ts: 1611221280 +yunmi.waterpuri.s5: + ts: 1607501816 +yunmi.waterpuri.x2: + ts: 1603963460 +yunmi.waterpuri.x7: + ts: 1603963565 +yunmi.waterpurifier.v2: + ts: 1632377061 +yunmi.waterpurifier.v3: + ts: 1611221428 +yyunyi.wopener.yypy24: + ts: 1616741966 +zdeer.ajh.a8: + ts: 1531108800 +zdeer.ajh.a9: + ts: 1531108800 +zdeer.ajh.zda10: + ts: 1531108800 +zdeer.ajh.zda9: + ts: 1531108800 +zdeer.ajh.zjy: + ts: 1531108800 +zhimi.aircondition.ma1: + ts: 1615185265 +zhimi.aircondition.ma3: + ts: 1615185279 +zhimi.aircondition.ma4: + ts: 1626334057 +zhimi.aircondition.v1: + ts: 1610610931 +zhimi.aircondition.va1: + ts: 1609924720 +zhimi.aircondition.za1: + ts: 1626334343 +zhimi.aircondition.za2: + ts: 1626334315 +zhimi.airfresh.va2: + ts: 1690860366 +zhimi.airfresh.va4: + ts: 1690860327 +zhimi.airmonitor.v1: + ts: 1621243516 +zhimi.airpurifier.m1: + ts: 1636711231 +zhimi.airpurifier.m2: + ts: 1636711316 +zhimi.airpurifier.ma2: + ts: 1636962519 +zhimi.airpurifier.mb1: + ts: 1604588729 +zhimi.airpurifier.mc1: + ts: 1644487928 +zhimi.airpurifier.sa2: + ts: 1635820002 +zhimi.airpurifier.v1: + ts: 1635855633 +zhimi.airpurifier.v3: + ts: 1676339933 +zhimi.airpurifier.v6: + ts: 1636978652 +zhimi.airpurifier.v7: + ts: 1605856617 +zhimi.airpurifier.v8: + ts: 1626168185 +zhimi.fan.sa1: + ts: 1700644763 +zhimi.fan.v2: + ts: 1615975066 +zhimi.fan.v3: + ts: 1689041686 +zhimi.fan.za1: + ts: 1604590500 +zhimi.fan.za3: + ts: 1689040855 +zhimi.fan.za4: + ts: 1689040869 +zhimi.heater.ma1: + ts: 1645683725 +zhimi.heater.za1: + ts: 1645683820 +zhimi.humidifier.ca1: + ts: 1636099783 +zhimi.humidifier.v1: + ts: 1639639102 +zhimi.lock.da1: + ts: 1614741765 +zhimi.lock.da2: + ts: 1614741779 +zhimi.toilet.sa1: + ts: 1608186583 +zhimi.toilet.va1: + ts: 1607503081 +zimi.clock.myk01: + ts: 1531108800 +zimi.mosq.v1: + ts: 1620728957 +zimi.powerstrip.v2: + ts: 1620812714 diff --git a/custom_components/xiaomi_home/miot/miot_lan.py b/custom_components/xiaomi_home/miot/miot_lan.py index 525be2f..07aadbc 100644 --- a/custom_components/xiaomi_home/miot/miot_lan.py +++ b/custom_components/xiaomi_home/miot/miot_lan.py @@ -71,7 +71,8 @@ from .miot_ev import MIoTEventLoop, TimeoutHandle from .miot_network import InterfaceStatus, MIoTNetwork, NetworkInfo from .miot_mdns import MipsService, MipsServiceState -from .common import randomize_int, MIoTMatcher +from .common import ( + randomize_int, load_yaml_file, gen_absolute_path, MIoTMatcher) _LOGGER = logging.getLogger(__name__) @@ -175,7 +176,7 @@ class MIoTLanDevice: OT_HEADER_LEN: int = 32 NETWORK_UNSTABLE_CNT_TH: int = 10 NETWORK_UNSTABLE_TIME_TH: int = 120000 - NETWORK_UNSTABLE_RESUME_TH: int = 300 + NETWORK_UNSTABLE_RESUME_TH: int = 300000 FAST_PING_INTERVAL: int = 5000 CONSTRUCT_STATE_PENDING: int = 15000 KA_INTERVAL_MIN = 10000 @@ -472,6 +473,8 @@ class MIoTLan: OT_PROBE_INTERVAL_MIN: int = 5000 OT_PROBE_INTERVAL_MAX: int = 45000 + PROFILE_MODELS_FILE: str = 'lan/profile_models.yaml' + _main_loop: asyncio.AbstractEventLoop _net_ifs: set[str] _network: MIoTNetwork @@ -502,6 +505,8 @@ class MIoTLan: _lan_state_sub_map: dict[str, Callable[[bool], asyncio.Future]] _lan_ctrl_vote_map: dict[str, bool] + _profile_models: dict[str, dict] + _init_done: bool def __init__( @@ -597,6 +602,12 @@ async def init_async(self) -> None: if self._net_ifs.isdisjoint(self._available_net_ifs): _LOGGER.info('no valid net_ifs') return + try: + self._profile_models = load_yaml_file( + 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 = {} self._mev = MIoTEventLoop() self._queue = queue.Queue() self._cmd_event_fd = os.eventfd(0, os.O_NONBLOCK) @@ -620,6 +631,7 @@ async def deinit_async(self) -> None: self.__lan_send_cmd(MIoTLanCmdType.DEINIT, None) self._thread.join() + self._profile_models = {} self._lan_devices = {} self._broadcast_socks = {} self._local_port = None @@ -1032,6 +1044,19 @@ def __cmd_read_handler(self, ctx: any) -> None: elif mips_cmd.type_ == MIoTLanCmdType.DEVICE_UPDATE: devices: dict[str, dict] = mips_cmd.data for did, info in devices.items(): + # did MUST be digit(UINT64) + if not did.isdigit(): + _LOGGER.info('invalid did, %s', did) + continue + 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')) + continue if did not in self._lan_devices: if 'token' not in info: _LOGGER.error( diff --git a/test/conftest.py b/test/conftest.py index e6583d9..9263402 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -43,6 +43,13 @@ def load_py_file(): dst=path.join(TEST_FILES_PATH, 'specs'), dirs_exist_ok=True) print('loaded spec test folder, specs') + # Copy lan files to test folder + shutil.copytree( + src=path.join( + TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/lan'), + dst=path.join(TEST_FILES_PATH, 'lan'), + dirs_exist_ok=True) + print('loaded lan test folder, lan') # Copy i18n files to test folder shutil.copytree( src=path.join( diff --git a/test/test_lan.py b/test/test_lan.py index e86a8be..4f96725 100755 --- a/test/test_lan.py +++ b/test/test_lan.py @@ -8,8 +8,37 @@ # pylint: disable=import-outside-toplevel, unused-argument +@pytest.mark.parametrize('test_devices', [{ + # specv2 model + '123456': { + 'token': '11223344556677d9a03d43936fc384205', + 'model': 'xiaomi.gateway.hub1' + }, + # profile model + '123457': { + 'token': '11223344556677d9a03d43936fc384205', + 'model': 'yeelink.light.lamp9' + }, + '123458': { + 'token': '11223344556677d9a03d43936fc384205', + 'model': 'zhimi.heater.ma1' + }, + # Non -digital did + 'group.123456': { + 'token': '11223344556677d9a03d43936fc384205', + 'model': 'mijia.light.group3' + }, + 'proxy.123456.1': { + 'token': '11223344556677d9a03d43936fc384205', + 'model': 'xiaomi.light.p1' + }, + 'miwifi_123456': { + 'token': '11223344556677d9a03d43936fc384205', + 'model': 'xiaomi.light.p1' + } +}]) @pytest.mark.asyncio -async def test_lan_async(): +async def test_lan_async(test_devices: dict): """ Use the central hub gateway as a test equipment, and through the local area network control central hub gateway indicator light switch. Please replace @@ -21,10 +50,13 @@ async def test_lan_async(): from miot.miot_lan import MIoTLan from miot.miot_mdns import MipsService - test_did = '' - test_token = '' + # Your central hub gateway did + test_did = '111111' + # Your central hub gateway did + test_token = '11223344556677d9a03d43936fc384205' test_model = 'xiaomi.gateway.hub1' - test_if_names = [''] + # Your computer interface list, such as enp3s0, wlp5s0 + test_if_names = ['enp3s0', 'wlp5s0'] # Check test params assert int(test_did) > 0 @@ -76,7 +108,8 @@ async def lan_state_change(state: bool): test_did: { 'token': test_token, 'model': test_model - } + }, + **test_devices }) # Test sub device state diff --git a/tools/common.py b/tools/common.py new file mode 100644 index 0000000..8dbb03a --- /dev/null +++ b/tools/common.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +"""Common functions.""" +import json +import yaml +from urllib.parse import urlencode +from urllib.request import Request, urlopen + + +def load_yaml_file(yaml_file: str) -> dict: + with open(yaml_file, 'r', encoding='utf-8') as file: + return yaml.safe_load(file) + + +def save_yaml_file(yaml_file: str, data: dict) -> None: + with open(yaml_file, 'w', encoding='utf-8') as file: + yaml.safe_dump( + data=data, stream=file, allow_unicode=True) + + +def load_json_file(json_file: str) -> dict: + with open(json_file, 'r', encoding='utf-8') as file: + return json.load(file) + + +def save_json_file(json_file: str, data: dict) -> None: + with open(json_file, 'w', encoding='utf-8') as file: + json.dump(data, file, ensure_ascii=False, indent=4) + + +def http_get( + url: str, params: dict = None, headers: dict = None +) -> dict: + if params: + encoded_params = urlencode(params) + full_url = f'{url}?{encoded_params}' + else: + full_url = url + request = Request(full_url, method='GET', headers=headers or {}) + content: bytes = None + with urlopen(request) as response: + content = response.read() + return ( + json.loads(str(content, 'utf-8')) + if content is not None else None) diff --git a/tools/update_lan_rule.py b/tools/update_lan_rule.py new file mode 100644 index 0000000..9f4d4fa --- /dev/null +++ b/tools/update_lan_rule.py @@ -0,0 +1,80 @@ +""" Update LAN rule.""" +# -*- coding: utf-8 -*- +# pylint: disable=relative-beyond-top-level +from os import path +from common import ( + http_get, + load_yaml_file, + save_yaml_file) + + +ROOT_PATH: str = path.dirname(path.abspath(__file__)) +LAN_PROFILE_MODELS_FILE: str = path.join( + ROOT_PATH, + '../custom_components/xiaomi_home/miot/lan/profile_models.yaml') + + +SPECIAL_MODELS: list[str] = [ + # model2class-v2 + 'chuangmi.camera.ipc007b', 'chuangmi.camera.ipc019b', + 'chuangmi.camera.ipc019e', 'chuangmi.camera.ipc020', + 'chuangmi.camera.v2', 'chuangmi.camera.v5', + 'chuangmi.camera.v6', 'chuangmi.camera.xiaobai', + 'chuangmi.radio.v1', 'chuangmi.radio.v2', + 'hith.foot_bath.q2', 'imou99.camera.tp2', + 'isa.camera.hl5', 'isa.camera.isc5', + 'jiqid.mistory.pro', 'jiqid.mistory.v1', + 'lumi.airrtc.tcpco2ecn01', 'lumi.airrtc.tcpecn02', + 'lumi.camera.gwagl01', 'miir.light.ir01', + 'miir.projector.ir01', 'miir.tv.hir01', + 'miir.tvbox.ir01', 'roome.bhf_light.yf6002', + 'smith.waterpuri.jnt600', 'viomi.fridge.u2', + 'xiaovv.camera.lamp', 'xiaovv.camera.ptz', + 'xiaovv.camera.xva3', 'xiaovv.camera.xvb4', + 'xiaovv.camera.xvsnowman', 'zdeer.ajh.a8', + 'zdeer.ajh.a9', 'zdeer.ajh.zda10', + 'zdeer.ajh.zda9', 'zdeer.ajh.zjy', 'zimi.clock.myk01', + # specialModels + 'chuangmi.camera.ipc004b', 'chuangmi.camera.ipc009', + 'chuangmi.camera.ipc010', 'chuangmi.camera.ipc013', + 'chuangmi.camera.ipc013d', 'chuangmi.camera.ipc016', + 'chuangmi.camera.ipc017', 'chuangmi.camera.ipc019', + 'chuangmi.camera.ipc021', 'chuangmi.camera.v3', + 'chuangmi.camera.v4', 'isa.camera.df3', + 'isa.camera.hlc6', 'lumi.acpartner.v1', + 'lumi.acpartner.v2', 'lumi.acpartner.v3', + 'lumi.airrtc.tcpecn01', 'lumi.camera.aq1', + 'miir.aircondition.ir01', 'miir.aircondition.ir02', + 'miir.fan.ir01', 'miir.stb.ir01', + 'miir.tv.ir01', 'mijia.camera.v1', + 'mijia.camera.v3', 'roborock.sweeper.s5v2', + 'roborock.vacuum.c1', 'roborock.vacuum.e2', + 'roborock.vacuum.m1s', 'roborock.vacuum.s5', + 'rockrobo.vacuum.v1', 'xiaovv.camera.xvd5'] + + +def update_profile_model(file_path: str): + profile_rules: dict = http_get( + url='https://miot-spec.org/instance/translate/models') + if not profile_rules and 'models' not in profile_rules and not isinstance( + profile_rules['models'], dict): + raise ValueError('Failed to get profile rule') + local_rules: dict = load_yaml_file( + yaml_file=file_path) or {} + for rule, ts in profile_rules['models'].items(): + if rule not in local_rules: + local_rules[rule] = {'ts': ts} + else: + local_rules[rule]['ts'] = ts + for mode in SPECIAL_MODELS: + if mode not in local_rules: + local_rules[mode] = {'ts': 1531108800} + else: + local_rules[mode]['ts'] = 1531108800 + local_rules = dict(sorted(local_rules.items())) + save_yaml_file( + yaml_file=file_path, data=local_rules) + + +update_profile_model(file_path=LAN_PROFILE_MODELS_FILE) +print('profile model list updated.') From 67785f747aaf4055028253e105ccfe9c38b447e8 Mon Sep 17 00:00:00 2001 From: Paul Shawn <32349595+topsworld@users.noreply.github.com> Date: Fri, 20 Dec 2024 21:20:49 +0800 Subject: [PATCH 40/44] doc: update changelog and version (#290) * doc: update changelog and version * doc: update changelog --- CHANGELOG.md | 11 +++++++++++ custom_components/xiaomi_home/manifest.json | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a14aec7..e1fdf52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # CHANGELOG +## v0.1.3 +### Added +### Changed +- Remove default bug label. [#276](https://github.com/XiaoMi/ha_xiaomi_home/pull/276) +- Improve multi-language translation actions. [#256](https://github.com/XiaoMi/ha_xiaomi_home/pull/256) +- Use aiohttp instead of waiting for blocking calls. [#227](https://github.com/XiaoMi/ha_xiaomi_home/pull/227) +- Language supports dt. [#237](https://github.com/XiaoMi/ha_xiaomi_home/pull/237) +### Fixed +- Fix local control error. [#271](https://github.com/XiaoMi/ha_xiaomi_home/pull/271) +- Fix README_zh and miot_storage. [#270](https://github.com/XiaoMi/ha_xiaomi_home/pull/270) + ## v0.1.2 ### Added - Support Xiaomi Heater devices. https://github.com/XiaoMi/ha_xiaomi_home/issues/124 https://github.com/XiaoMi/ha_xiaomi_home/issues/117 diff --git a/custom_components/xiaomi_home/manifest.json b/custom_components/xiaomi_home/manifest.json index 5f55d15..19afdc2 100644 --- a/custom_components/xiaomi_home/manifest.json +++ b/custom_components/xiaomi_home/manifest.json @@ -26,8 +26,8 @@ "psutil", "aiohttp[speedups]" ], - "version": "v0.1.2", + "version": "v0.1.3", "zeroconf": [ "_miot-central._tcp.local." ] -} +} \ No newline at end of file From f87e746188ad6f6411b1e83733cd4d02e5fb1285 Mon Sep 17 00:00:00 2001 From: Paul Shawn <32349595+topsworld@users.noreply.github.com> Date: Sat, 21 Dec 2024 10:42:48 +0800 Subject: [PATCH 41/44] fix: fix miot cloud token refresh (#307) --- custom_components/xiaomi_home/miot/miot_cloud.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/xiaomi_home/miot/miot_cloud.py b/custom_components/xiaomi_home/miot/miot_cloud.py index dd4c282..2d5c49c 100644 --- a/custom_components/xiaomi_home/miot/miot_cloud.py +++ b/custom_components/xiaomi_home/miot/miot_cloud.py @@ -204,7 +204,7 @@ async def refresh_access_token_async(self, refresh_token: str) -> dict: if not isinstance(refresh_token, str): raise MIoTOauthError('invalid refresh_token') - return await self._get_token_async(data={ + return await self.__get_token_async(data={ 'client_id': self._client_id, 'redirect_uri': self._redirect_url, 'refresh_token': refresh_token, @@ -661,7 +661,6 @@ async def get_props_async(self, params: list) -> list: 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}]) From 02ddf8df56d1392a03d7dfb74c659202f469d844 Mon Sep 17 00:00:00 2001 From: Paul Shawn <32349595+topsworld@users.noreply.github.com> Date: Sat, 21 Dec 2024 11:08:36 +0800 Subject: [PATCH 42/44] fix: fix lan ctrl filter logic (#303) * style: rename devices_filter to homes_select * fix: fix miot_lan update_devices * fix: async blocking call --- custom_components/xiaomi_home/config_flow.py | 36 +++++++++---------- .../xiaomi_home/miot/miot_client.py | 2 ++ .../xiaomi_home/miot/miot_lan.py | 5 +-- .../xiaomi_home/translations/de.json | 4 +-- .../xiaomi_home/translations/en.json | 4 +-- .../xiaomi_home/translations/es.json | 4 +-- .../xiaomi_home/translations/fr.json | 4 +-- .../xiaomi_home/translations/ja.json | 4 +-- .../xiaomi_home/translations/nl.json | 4 +-- .../xiaomi_home/translations/pt-BR.json | 4 +-- .../xiaomi_home/translations/pt.json | 4 +-- .../xiaomi_home/translations/ru.json | 4 +-- .../xiaomi_home/translations/zh-Hans.json | 4 +-- .../xiaomi_home/translations/zh-Hant.json | 4 +-- 14 files changed, 45 insertions(+), 42 deletions(-) diff --git a/custom_components/xiaomi_home/config_flow.py b/custom_components/xiaomi_home/config_flow.py index c1e4e67..b9f3776 100644 --- a/custom_components/xiaomi_home/config_flow.py +++ b/custom_components/xiaomi_home/config_flow.py @@ -315,7 +315,7 @@ async def async_step_oauth(self, user_input=None): _LOGGER.error('task_oauth exception, %s', error) self._config_error_reason = str(error) return self.async_show_progress_done(next_step_id='oauth_error') - return self.async_show_progress_done(next_step_id='devices_filter') + return self.async_show_progress_done(next_step_id='homes_select') return self.async_show_progress( step_id='oauth', progress_action='oauth', @@ -469,15 +469,15 @@ async def async_step_oauth_error(self, user_input=None): errors={'base': error_reason}, ) - async def async_step_devices_filter(self, user_input=None): - _LOGGER.debug('async_step_devices_filter') + async def async_step_homes_select(self, user_input=None): + _LOGGER.debug('async_step_homes_select') try: if user_input is None: - return await self.display_device_filter_form('') + return await self.display_homes_select_form('') home_selected: list = user_input.get('home_infos', []) if not home_selected: - return await self.display_device_filter_form( + return await self.display_homes_select_form( 'no_family_selected') self._ctrl_mode = user_input.get('ctrl_mode') for home_id, home_info in self._home_info_buffer[ @@ -495,7 +495,7 @@ async def async_step_devices_filter(self, user_input=None): for did, dev_info in self._home_info_buffer['devices'].items() if dev_info['home_id'] in home_selected} if not devices_list: - return await self.display_device_filter_form('no_devices') + return await self.display_homes_select_form('no_devices') devices_list_sort = dict(sorted( devices_list.items(), key=lambda item: item[1].get('home_id', '')+item[1].get('room_id', ''))) @@ -506,7 +506,7 @@ async def async_step_devices_filter(self, user_input=None): _LOGGER.error( 'save devices async failed, %s, %s', self._uid, self._cloud_server) - return await self.display_device_filter_form( + return await self.display_homes_select_form( 'devices_storage_failed') if not (await self._miot_storage.update_user_config_async( uid=self._uid, cloud_server=self._cloud_server, config={ @@ -535,7 +535,7 @@ async def async_step_devices_filter(self, user_input=None): }) except Exception as err: _LOGGER.error( - 'async_step_devices_filter, %s, %s', + 'async_step_homes_select, %s, %s', err, traceback.format_exc()) raise AbortFlow( reason='config_flow_error', @@ -543,9 +543,9 @@ async def async_step_devices_filter(self, user_input=None): 'error': f'config_flow error, {err}'} ) from err - async def display_device_filter_form(self, reason: str): + async def display_homes_select_form(self, reason: str): return self.async_show_form( - step_id='devices_filter', + step_id='homes_select', data_schema=vol.Schema({ vol.Required('ctrl_mode', default=DEFAULT_CTRL_MODE): vol.In( self._miot_i18n.translate(key='config.control_mode')), @@ -941,7 +941,7 @@ async def async_step_config_options(self, user_input=None): async def async_step_update_user_info(self, user_input=None): if not self._update_user_info: - return await self.async_step_devices_filter() + return await self.async_step_homes_select() if not user_input: nick_name_new = ( await self._miot_http.get_user_info_async() or {}).get( @@ -958,9 +958,9 @@ async def async_step_update_user_info(self, user_input=None): ) self._nick_name_new = user_input.get('nick_name') - return await self.async_step_devices_filter() + return await self.async_step_homes_select() - async def async_step_devices_filter(self, user_input=None): + async def async_step_homes_select(self, user_input=None): if not self._update_devices: return await self.async_step_update_trans_rules() if not user_input: @@ -1012,11 +1012,11 @@ async def async_step_devices_filter(self, user_input=None): if home_id in home_list] self._home_list = dict(sorted(home_list.items())) - return await self.display_device_filter_form('') + return await self.display_homes_select_form('') self._home_selected_list = user_input.get('home_infos', []) if not self._home_selected_list: - return await self.display_device_filter_form('no_family_selected') + return await self.display_homes_select_form('no_family_selected') self._ctrl_mode = user_input.get('ctrl_mode') self._home_selected_dict = {} for home_id, home_info in self._home_info_buffer[ @@ -1029,7 +1029,7 @@ async def async_step_devices_filter(self, user_input=None): for did, dev_info in self._home_info_buffer['devices'].items() if dev_info['home_id'] in self._home_selected_list} if not self._device_list: - return await self.display_device_filter_form('no_devices') + return await self.display_homes_select_form('no_devices') # Statistics devices changed self._devices_add = [] self._devices_remove = [] @@ -1047,9 +1047,9 @@ async def async_step_devices_filter(self, user_input=None): self._devices_add, self._devices_remove) return await self.async_step_update_trans_rules() - async def display_device_filter_form(self, reason: str): + async def display_homes_select_form(self, reason: str): return self.async_show_form( - step_id='devices_filter', + step_id='homes_select', data_schema=vol.Schema({ vol.Required( 'ctrl_mode', default=self._ctrl_mode diff --git a/custom_components/xiaomi_home/miot/miot_client.py b/custom_components/xiaomi_home/miot/miot_client.py index 31c87f0..7423e20 100644 --- a/custom_components/xiaomi_home/miot/miot_client.py +++ b/custom_components/xiaomi_home/miot/miot_client.py @@ -1028,6 +1028,7 @@ async def __on_miot_lan_state_change(self, state: bool) -> None: self._miot_lan.update_devices(devices={ did: { 'token': info['token'], + 'model': info['model'], 'connect_type': info['connect_type']} for did, info in self._device_list_cache.items() if 'token' in info and 'connect_type' in info @@ -1291,6 +1292,7 @@ async def __refresh_cloud_devices_async(self) -> None: self._miot_lan.update_devices(devices={ did: { 'token': info['token'], + 'model': info['model'], 'connect_type': info['connect_type']} for did, info in self._device_list_cache.items() if 'token' in info and 'connect_type' in info diff --git a/custom_components/xiaomi_home/miot/miot_lan.py b/custom_components/xiaomi_home/miot/miot_lan.py index 07aadbc..6679328 100644 --- a/custom_components/xiaomi_home/miot/miot_lan.py +++ b/custom_components/xiaomi_home/miot/miot_lan.py @@ -603,8 +603,9 @@ async def init_async(self) -> None: _LOGGER.info('no valid net_ifs') return try: - self._profile_models = load_yaml_file( - yaml_file=gen_absolute_path(self.PROFILE_MODELS_FILE)) + self._profile_models = await self._main_loop.run_in_executor( + 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 = {} diff --git a/custom_components/xiaomi_home/translations/de.json b/custom_components/xiaomi_home/translations/de.json index 226aa57..b06c460 100644 --- a/custom_components/xiaomi_home/translations/de.json +++ b/custom_components/xiaomi_home/translations/de.json @@ -22,7 +22,7 @@ "title": "Fehler bei der Anmeldung", "description": "Klicken Sie auf \"Weiter\", um es erneut zu versuchen." }, - "devices_filter": { + "homes_select": { "title": "Familie und Geräte auswählen", "description": "## Gebrauchsanweisung\r\n### Steuerungsmodus\r\n- Automatisch: Wenn im lokalen Netzwerk ein verfügbarer Xiaomi-Zentralgateway vorhanden ist, wird Home Assistant bevorzugt Steuerbefehle über den Zentralgateway senden, um eine lokale Steuerung zu ermöglichen. Wenn im lokalen Netzwerk kein Zentralgateway vorhanden ist, wird versucht, Steuerbefehle über das Xiaomi-OT-Protokoll zu senden, um eine lokale Steuerung zu ermöglichen. Nur wenn die oben genannten Bedingungen für die lokale Steuerung nicht erfüllt sind, werden die Steuerbefehle über die Cloud gesendet.\r\n- Cloud: Steuerbefehle werden nur über die Cloud gesendet.\r\n### Familienimport für importierte Geräte\r\nDie Integration fügt Geräte aus den ausgewählten Familien hinzu.\r\n### Raumnamensynchronisationsmodus\r\nWenn Geräte von der Xiaomi Home App zu Home Assistant synchronisiert werden, wird die Bezeichnung des Bereichs, in dem sich die Geräte in Home Assistant befinden, nach folgenden Regeln benannt. Beachten Sie, dass das Synchronisieren von Geräten den von Xiaomi Home App festgelegten Familien- und Raum-Einstellungen nicht ändert.\r\n- Nicht synchronisieren: Das Gerät wird keinem Bereich hinzugefügt.\r\n- Andere Optionen: Der Bereich, in den das Gerät aufgenommen wird, wird nach dem Namen der Familie oder des Raums in der Xiaomi Home App benannt.\r\n### Action-Debug-Modus\r\nFür von MIoT-Spec-V2 definierte Gerätemethoden wird neben der Benachrichtigungs-Entität auch eine Texteingabe-Entität generiert. Damit können Sie bei der Fehlerbehebung Steuerbefehle an das Gerät senden.\r\n### Verstecke Nicht-Standard-Entitäten\r\nVerstecke Entitäten, die von nicht standardmäßigen MIoT-Spec-V2-Instanzen mit einem Namen beginnen, der mit einem \"*\" beginnt.\r\n\r\n \r\n### Hallo {nick_name}! Bitte wählen Sie den Steuerungsmodus der Integration sowie die Familie aus, in der sich die hinzuzufügenden Geräte befinden.", "data": { @@ -87,7 +87,7 @@ "nick_name": "Benutzername" } }, - "devices_filter": { + "homes_select": { "title": "Familie und Geräte neu auswählen", "description": "## Gebrauchsanweisung\r\n### Steuerungsmodus\r\n- Automatisch: Wenn im lokalen Netzwerk ein verfügbarer Xiaomi-Zentralgateway vorhanden ist, wird Home Assistant bevorzugt Steuerbefehle über den Zentralgateway senden, um eine lokale Steuerung zu ermöglichen. Wenn im lokalen Netzwerk kein Zentralgateway vorhanden ist, wird versucht, Steuerbefehle über das Xiaomi-OT-Protokoll zu senden, um eine lokale Steuerung zu ermöglichen. Nur wenn die oben genannten Bedingungen für die lokale Steuerung nicht erfüllt sind, werden die Steuerbefehle über die Cloud gesendet.\r\n- Cloud: Steuerbefehle werden nur über die Cloud gesendet.\r\n### Familienimport für importierte Geräte\r\nDie Integration fügt Geräte aus den ausgewählten Familien hinzu.\r\n \r\n### Hallo {nick_name}! Bitte wählen Sie den Steuerungsmodus der Integration sowie die Familie aus, in der sich die hinzuzufügenden Geräte befinden.", "data": { diff --git a/custom_components/xiaomi_home/translations/en.json b/custom_components/xiaomi_home/translations/en.json index 8fe89c5..ddb424a 100644 --- a/custom_components/xiaomi_home/translations/en.json +++ b/custom_components/xiaomi_home/translations/en.json @@ -22,7 +22,7 @@ "title": "Login Error", "description": "Click NEXT to try again." }, - "devices_filter": { + "homes_select": { "title": "Select Home and Devices", "description": "## Usage Instructions\r\n### Control mode\r\n- Auto: When there is an available Xiaomi central hub gateway in the local area network, Home Assistant will prioritize sending device control commands through the central hub gateway to achieve local control. If there is no central hub gateway in the local area network, it will attempt to send control commands through Xiaomi LAN control function. Only when the above local control conditions are not met, the device control commands will be sent through the cloud.\r\n- Cloud: All control commands are sent through the cloud.\r\n### Import devices from home\r\nThe integration will add devices from the selected homes.\n### Room name synchronizing mode\nWhen importing devices from Xiaomi Home APP to Home Assistant, the naming convention of the area where the device is added to is as follows. Note that the device synchronizing process does not change the home or room settings in Xiaomi Home APP.\r\n- Do not synchronize: The device will not be added to any area.\r\n- Other options: The device will be added to an area named as the home and/or room name that already exists in Xiaomi Home APP.\r\n### Debug mode for action\r\nFor the action defined in MIoT-Spec-V2 of the device, a Text entity along with a Notify entity will be created, in which you can send control commands to the device for debugging.\r\n### Hide non-standard created entities\r\nHide the entities generated from non-standard MIoT-Spec-V2 instances, whose names begin with \"*\".\r\n\r\n \r\n### Hello {nick_name}, please select the integration control mode and the home where the device you want to import.", "data": { @@ -87,7 +87,7 @@ "nick_name": "Nick Name" } }, - "devices_filter": { + "homes_select": { "title": "Re-select Home and Devices", "description": "## Usage Instructions\r\n### Control mode\r\n- Auto: When there is an available Xiaomi central hub gateway in the local area network, Home Assistant will prioritize sending device control commands through the central hub gateway to achieve local control. If there is no central hub gateway in the local area network, it will attempt to send control commands through Xiaomi LAN control function. Only when the above local control conditions are not met, the device control commands will be sent through the cloud.\r\n- Cloud: All control commands are sent through the cloud.\r\n### Import devices from home\r\nThe integration will add devices from the selected homes.\r\n \r\n### Hello {nick_name}, please select the integration control mode and the home where the device you want to import.", "data": { diff --git a/custom_components/xiaomi_home/translations/es.json b/custom_components/xiaomi_home/translations/es.json index 22fd8a5..23ee109 100644 --- a/custom_components/xiaomi_home/translations/es.json +++ b/custom_components/xiaomi_home/translations/es.json @@ -22,7 +22,7 @@ "title": "Error de inicio de sesión", "description": "Haga clic en \"Siguiente\" para volver a intentarlo" }, - "devices_filter": { + "homes_select": { "title": "Seleccionar hogares y dispositivos", "description": "## Instrucciones de uso\r\n### Modo de control\r\n- Automático: Cuando hay un gateway central de Xiaomi disponible en la red local, Home Assistant priorizará el envío de comandos de control de dispositivos a través del gateway central para lograr un control localizado. Si no hay un gateway central en la red local, intentará enviar comandos de control a través del protocolo Xiaomi OT para lograr un control localizado. Solo cuando no se cumplan las condiciones anteriores de control localizado, los comandos de control del dispositivo se enviarán a través de la nube.\r\n- Nube: Los comandos de control solo se envían a través de la nube.\r\n### Hogares de dispositivos importados\r\nLa integración agregará los dispositivos en los hogares seleccionados.\r\n### Modo de sincronización del nombre de la habitación\r\nCuando se sincronizan los dispositivos desde la aplicación Xiaomi Home a Home Assistant, los nombres de las áreas donde se encuentran los dispositivos en Home Assistant seguirán las reglas de nomenclatura a continuación. Tenga en cuenta que el proceso de sincronización de dispositivos no cambiará la configuración de hogares y habitaciones en la aplicación Xiaomi Home.\r\n- Sin sincronización: el dispositivo no se agregará a ninguna área.\r\n- Otras opciones: la zona donde se agrega el dispositivo tendrá el mismo nombre que el hogar o la habitación en la aplicación Xiaomi Home.\r\n### Modo de depuración de Action\r\nPara los métodos definidos por MIoT-Spec-V2, además de generar una entidad de notificación, también se generará una entidad de cuadro de entrada de texto que se puede utilizar para enviar comandos de control al dispositivo durante la depuración.\r\n### Ocultar entidades generadas no estándar\r\nOcultar las entidades generadas por la instancia no estándar MIoT-Spec-V2 que comienzan con \"*\".\r\n\r\n \r\n### ¡Hola, {nick_name}! Seleccione el modo de control de integración y el hogar donde se encuentran los dispositivos que desea agregar.", "data": { @@ -87,7 +87,7 @@ "nick_name": "Apodo de usuario" } }, - "devices_filter": { + "homes_select": { "title": "Recomendar hogares y dispositivos", "description": "## Instrucciones de uso\r\n### Modo de control\r\n- Automático: Cuando hay un gateway central de Xiaomi disponible en la red local, Home Assistant priorizará el envío de comandos de control de dispositivos a través del gateway central para lograr un control localizado. Si no hay un gateway central en la red local, intentará enviar comandos de control a través del protocolo Xiaomi OT para lograr un control localizado. Solo cuando no se cumplan las condiciones anteriores de control localizado, los comandos de control del dispositivo se enviarán a través de la nube.\r\n- Nube: Los comandos de control solo se envían a través de la nube.\r\n### Hogares de dispositivos importados\r\nLa integración agregará los dispositivos en los hogares seleccionados.\r\n \r\n### ¡Hola, {nick_name}! Seleccione el modo de control de integración y el hogar donde se encuentran los dispositivos que desea agregar.", "data": { diff --git a/custom_components/xiaomi_home/translations/fr.json b/custom_components/xiaomi_home/translations/fr.json index 9be87b8..82c8908 100644 --- a/custom_components/xiaomi_home/translations/fr.json +++ b/custom_components/xiaomi_home/translations/fr.json @@ -22,7 +22,7 @@ "title": "Erreur de connexion", "description": "Cliquez sur \"Suivant\" pour réessayer" }, - "devices_filter": { + "homes_select": { "title": "Sélectionner une maison et des appareils", "description": "## Instructions d'utilisation\r\n### Mode de contrôle\r\n- Automatique: Lorsqu'il y a une passerelle centrale Xiaomi disponible dans le réseau local, Home Assistant priorisera l'envoi des commandes de contrôle des appareils via la passerelle centrale pour réaliser un contrôle localisé. S'il n'y a pas de passerelle centrale dans le réseau local, il tentera d'envoyer des commandes de contrôle via le protocole Xiaomi OT pour réaliser un contrôle localisé. Ce n'est que lorsque les conditions de contrôle localisé ci-dessus ne sont pas remplies que les commandes de contrôle des appareils seront envoyées via le cloud.\r\n- Cloud: Les commandes de contrôle ne sont envoyées que via le cloud.\r\n### Importer une maison pour les appareils\r\nL'intégration ajoutera les appareils de la maison sélectionnée.\r\n### Mode de synchronisation des noms de pièces\r\nLors de la synchronisation des appareils de Xiaomi Home à Home Assistant, le nom de la pièce où se trouve l'appareil sera nommé selon les règles suivantes. Notez que le processus de synchronisation des appareils n'affecte pas les paramètres de la maison et de la pièce dans Xiaomi Home APP.\r\n- Ne pas synchroniser: L'appareil ne sera ajouté à aucune zone.\r\n- Autre option: La zone dans laquelle l'appareil est ajouté est nommée en fonction du nom de la maison ou de la pièce de Xiaomi Home APP.\r\n### Mode de débogage d'action\r\nPour les méthodes définies par MIoT-Spec-V2, en plus de générer une entité de notification, une entité de zone de texte sera également générée pour que vous puissiez envoyer des commandes de contrôle à l'appareil lors du débogage.\r\n### Masquer les entités générées non standard\r\nMasquer les entités générées non standard de MIoT-Spec-V2 commençant par \"*\".\r\n\r\n \r\n### {nick_name} Bonjour ! Veuillez sélectionner le mode de contrôle de l'intégration et la maison où se trouvent les appareils à ajouter.", "data": { @@ -87,7 +87,7 @@ "nick_name": "Pseudo utilisateur" } }, - "devices_filter": { + "homes_select": { "title": "Re-sélectionner une maison et des appareils", "description": "## Instructions d'utilisation\r\n### Mode de contrôle\r\n- Automatique: Lorsqu'il y a une passerelle centrale Xiaomi disponible dans le réseau local, Home Assistant priorisera l'envoi des commandes de contrôle des appareils via la passerelle centrale pour réaliser un contrôle localisé. S'il n'y a pas de passerelle centrale dans le réseau local, il tentera d'envoyer des commandes de contrôle via le protocole Xiaomi OT pour réaliser un contrôle localisé. Ce n'est que lorsque les conditions de contrôle localisé ci-dessus ne sont pas remplies que les commandes de contrôle des appareils seront envoyées via le cloud.\r\n- Cloud: Les commandes de contrôle ne sont envoyées que via le cloud.\r\n### Importer une maison pour les appareils\r\nL'intégration ajoutera les appareils de la maison sélectionnée.\r\n \r\n### {nick_name} Bonjour ! Veuillez sélectionner le mode de contrôle de l'intégration et la maison où se trouvent les appareils à ajouter.", "data": { diff --git a/custom_components/xiaomi_home/translations/ja.json b/custom_components/xiaomi_home/translations/ja.json index 780378d..a8d6b36 100644 --- a/custom_components/xiaomi_home/translations/ja.json +++ b/custom_components/xiaomi_home/translations/ja.json @@ -22,7 +22,7 @@ "title": "ログインエラー", "description": "「次へ」をクリックして再試行してください" }, - "devices_filter": { + "homes_select": { "title": "ホームとデバイスを選択", "description": "## 使用方法\r\n### 制御モード\r\n- 自動: ローカルエリアネットワーク内に利用可能なXiaomi中央ゲートウェイが存在する場合、Home Assistantは中央ゲートウェイを介してデバイス制御コマンドを優先的に送信し、ローカル制御機能を実現します。ローカルエリアネットワーク内に中央ゲートウェイが存在しない場合、Xiaomi OTプロトコルを介して制御コマンドを送信し、ローカル制御機能を実現しようとします。上記のローカル制御条件が満たされない場合にのみ、デバイス制御コマンドはクラウドを介して送信されます。\r\n- クラウド: 制御コマンドはクラウドを介してのみ送信されます。\r\n### 導入されたデバイスのホーム\r\n統合は、選択された家庭にあるデバイスを追加します。\r\n### 部屋名同期モード\r\nXiaomi Home アプリから Home Assistant に同期されるデバイスの場合、デバイスが Home Assistant 内でどのような領域にあるかを示す名前の命名方式は、以下のルールに従います。ただし、デバイスの同期プロセスは、Xiaomi Home アプリで家庭および部屋の設定を変更しないことに注意してください。\r\n- 同期しない:デバイスはどの領域にも追加されません。\r\n- その他のオプション:デバイスが追加される領域は、Xiaomi Home アプリの家庭または部屋の名前に従って命名されます。\r\n### Action デバッグモード\r\nデバイスが MIoT-Spec-V2 で定義された方法を実行する場合、通知エンティティの生成に加えて、テキスト入力ボックスエンティティも生成されます。これを使用して、デバイスに制御命令を送信することができます。\r\n### 非標準生成エンティティを非表示にする\r\n「*」で始まる名前の非標準 MIoT-Spec-V2 インスタンスによって生成されたエンティティを非表示にします。\r\n\r\n \r\n### {nick_name} さん、こんにちは! 統合制御モードと追加するデバイスがあるホームを選択してください。", "data": { @@ -87,7 +87,7 @@ "nick_name": "ユーザー名" } }, - "devices_filter": { + "homes_select": { "title": "ホームとデバイスを再度選択", "description": "## 使用方法\r\n### 制御モード\r\n- 自動: ローカルエリアネットワーク内に利用可能なXiaomi中央ゲートウェイが存在する場合、Home Assistantは中央ゲートウェイを介してデバイス制御コマンドを優先的に送信し、ローカル制御機能を実現します。ローカルエリアネットワーク内に中央ゲートウェイが存在しない場合、Xiaomi OTプロトコルを介して制御コマンドを送信し、ローカル制御機能を実現しようとします。上記のローカル制御条件が満たされない場合にのみ、デバイス制御コマンドはクラウドを介して送信されます。\r\n- クラウド: 制御コマンドはクラウドを介してのみ送信されます。\r\n### 導入されたデバイスのホーム\r\n統合は、選択された家庭にあるデバイスを追加します。\r\n \r\n### {nick_name} さん、こんにちは! 統合制御モードと追加するデバイスがあるホームを選択してください。", "data": { diff --git a/custom_components/xiaomi_home/translations/nl.json b/custom_components/xiaomi_home/translations/nl.json index aa15df8..a01f845 100644 --- a/custom_components/xiaomi_home/translations/nl.json +++ b/custom_components/xiaomi_home/translations/nl.json @@ -22,7 +22,7 @@ "title": "Inlogfout", "description": "Klik OP VOLGENDE om het opnieuw te proberen." }, - "devices_filter": { + "homes_select": { "title": "Selecteer Huis en Apparaten", "description": "## Gebruiksinstructies\r\n### Controlemodus\r\n- Auto: Wanneer er een beschikbare Xiaomi centrale hubgateway in het lokale netwerk is, geeft Home Assistant de voorkeur aan het verzenden van apparaatbedieningscommando's via de centrale hubgateway om lokale controle te bereiken. Als er geen centrale hubgateway in het lokale netwerk is, zal het proberen bedieningscommando's te verzenden via de Xiaomi LAN-controlefunctie. Alleen wanneer de bovenstaande lokale controlevoorwaarden niet zijn vervuld, worden de apparaatbedieningscommando's via de cloud verzonden.\r\n- Cloud: Alle bedieningscommando's worden via de cloud verzonden.\r\n### Apparaten importeren vanuit huis\r\nDe integratie voegt apparaten toe van de geselecteerde huizen.\n### Ruimtenaamsynchronisatiemodus\nBij het importeren van apparaten vanuit de Xiaomi Home APP naar Home Assistant is de naamgevingsconventie van het gebied waarin het apparaat wordt toegevoegd als volgt. Opmerking: het synchronisatieproces van het apparaat verandert de huis- of ruimte-instellingen in de Xiaomi Home APP niet.\r\n- Niet synchroniseren: Het apparaat wordt aan geen enkel gebied toegevoegd.\r\n- Andere opties: Het apparaat wordt toegevoegd aan een gebied dat is genoemd naar de huis- en/of ruimtenamen die al bestaan in de Xiaomi Home APP.\r\n### Debugmodus voor actie\r\nVoor de actie gedefinieerd in MIoT-Spec-V2 van het apparaat, wordt er een Tekstentiteit samen met een Notificatie-entiteit aangemaakt, waarin u bedieningscommando's naar het apparaat kunt sturen voor debugging.\r\n### Verberg niet-standaard gemaakte entiteiten\r\nVerberg de entiteiten die zijn gegenereerd vanuit niet-standaard MIoT-Spec-V2-instanties, waarvan de namen beginnen met \"*\".\r\n\r\n \r\n### Hallo {nick_name}, selecteer alstublieft de integratie controlemethodiek en het huis waar het apparaat dat u wilt importeren zich bevindt.", "data": { @@ -87,7 +87,7 @@ "nick_name": "Bijnaam" } }, - "devices_filter": { + "homes_select": { "title": "Huis en Apparaten opnieuw selecteren", "description": "## Gebruiksinstructies\r\n### Controlemodus\r\n- Auto: Wanneer er een beschikbare Xiaomi centrale hubgateway in het lokale netwerk is, geeft Home Assistant de voorkeur aan het verzenden van apparaatbedieningscommando's via de centrale hubgateway om lokale controle te bereiken. Als er geen centrale hubgateway in het lokale netwerk is, zal het proberen bedieningscommando's te verzenden via de Xiaomi LAN-controlefunctie. Alleen wanneer de bovenstaande lokale controlevoorwaarden niet zijn vervuld, worden de apparaatbedieningscommando's via de cloud verzonden.\r\n- Cloud: Alle bedieningscommando's worden via de cloud verzonden.\r\n### Apparaten importeren vanuit huis\r\nDe integratie voegt apparaten toe van de geselecteerde huizen.\r\n \r\n### Hallo {nick_name}, selecteer alstublieft de integratie controlemethodiek en het huis waar het apparaat dat u wilt importeren zich bevindt.", "data": { diff --git a/custom_components/xiaomi_home/translations/pt-BR.json b/custom_components/xiaomi_home/translations/pt-BR.json index 4ae8c2e..fca54a5 100644 --- a/custom_components/xiaomi_home/translations/pt-BR.json +++ b/custom_components/xiaomi_home/translations/pt-BR.json @@ -22,7 +22,7 @@ "title": "Erro de Login", "description": "Clique em AVANÇAR para tentar novamente." }, - "devices_filter": { + "homes_select": { "title": "Selecione a Casa e os Dispositivos", "description": "## Instruções de Uso\r\n### Modo de controle\r\n- Auto: Quando houver um gateway central Xiaomi disponível na rede local, o Home Assistant priorizará o envio de comandos de controle do dispositivo através do gateway central, obtendo assim controle local. Se não houver gateway central na rede local, ele tentará enviar comandos através da função de controle LAN da Xiaomi. Somente quando as condições de controle local acima não forem atendidas, os comandos serão enviados pela nuvem.\r\n- Nuvem: Todos os comandos de controle são enviados através da nuvem.\r\n### Importar dispositivos da casa\r\nA integração adicionará dispositivos das casas selecionadas.\n### Modo de sincronização do nome do cômodo\r\nAo importar dispositivos do aplicativo Xiaomi Home para o Home Assistant, a convenção de nomeação da área onde o dispositivo é adicionado é a seguinte. Observe que o processo de sincronização do dispositivo não altera as configurações de casa ou cômodo no aplicativo Xiaomi Home.\r\n- Não sincronizar: O dispositivo não será adicionado a nenhuma área.\r\n- Outras opções: O dispositivo será adicionado a uma área nomeada de acordo com o nome da casa e/ou do cômodo que já existem no aplicativo Xiaomi Home.\r\n### Modo de depuração para ação\r\nPara as ações definidas no MIoT-Spec-V2 do dispositivo, será criada uma entidade de texto juntamente com uma entidade de notificação, nas quais você poderá enviar comandos de controle ao dispositivo para fins de depuração.\r\n### Ocultar entidades criadas não padrão\r\nOculta as entidades geradas a partir de instâncias não padrão do MIoT-Spec-V2, cujos nomes começam com \"*\".\r\n\r\n \r\n### Olá {nick_name}, selecione o modo de controle da integração e a casa onde estão os dispositivos que você deseja importar.", "data": { @@ -87,7 +87,7 @@ "nick_name": "Apelido" } }, - "devices_filter": { + "homes_select": { "title": "Selecionar novamente Casa e Dispositivos", "description": "## Instruções de Uso\r\n### Modo de controle\r\n- Auto: Quando houver um gateway central Xiaomi disponível na rede local, o Home Assistant priorizará o envio de comandos através dele para obter controle local. Caso não haja, tentará enviar comandos através da função de controle LAN da Xiaomi. Somente se as condições anteriores não forem atendidas, o controle será feito pela nuvem.\r\n- Nuvem: Todos os comandos de controle são enviados pela nuvem.\r\n### Importar dispositivos da casa\r\nA integração adicionará dispositivos das casas selecionadas.\r\n \r\n### Olá {nick_name}, selecione o modo de controle da integração e a casa de onde deseja importar dispositivos.", "data": { diff --git a/custom_components/xiaomi_home/translations/pt.json b/custom_components/xiaomi_home/translations/pt.json index ca72df4..480f59e 100644 --- a/custom_components/xiaomi_home/translations/pt.json +++ b/custom_components/xiaomi_home/translations/pt.json @@ -22,7 +22,7 @@ "title": "Erro de Login", "description": "Clique em SEGUINTE para tentar novamente." }, - "devices_filter": { + "homes_select": { "title": "Selecione a Casa e os Dispositivos", "description": "## Instruções de Utilização\r\n### Modo de Controlo\r\n- Automático: Quando existir um gateway central Xiaomi disponível na rede local, o Home Assistant dará prioridade ao envio de comandos de controlo através do gateway central, permitindo um controlo local. Caso não exista um gateway central na rede local, tentará enviar comandos através da funcionalidade de controlo LAN. Apenas se estas condições não forem cumpridas, os comandos serão enviados pela nuvem.\r\n- Nuvem: Todos os comandos de controlo são enviados através da nuvem.\r\n### Importar dispositivos da casa\r\nA integração adicionará dispositivos das casas selecionadas.\n### Modo de sincronização do nome da divisão\r\nAo importar dispositivos da aplicação Xiaomi Home para o Home Assistant, a nomeação da área onde o dispositivo é adicionado segue as regras abaixo. Note que o processo de sincronização dos dispositivos não altera as definições de casa ou divisão na aplicação Xiaomi Home.\r\n- Não sincronizar: O dispositivo não será atribuído a qualquer área.\r\n- Outras opções: O dispositivo será adicionado a uma área cujo nome corresponde ao da casa e/ou divisão definida na aplicação Xiaomi Home.\r\n### Modo de depuração de ação\r\nPara as ações definidas no MIoT-Spec-V2 do dispositivo, será criada uma entidade do tipo texto juntamente com uma entidade de notificação, nas quais poderá enviar comandos de controlo ao dispositivo para fins de depuração.\r\n### Ocultar entidades não padrão\r\nOculta as entidades geradas a partir de instâncias não padrão do MIoT-Spec-V2, cujos nomes começam por \"*\".\r\n\r\n \r\n### Olá {nick_name}, selecione o modo de controlo da integração e a casa na qual deseja importar os dispositivos.", "data": { @@ -87,7 +87,7 @@ "nick_name": "Alcunha" } }, - "devices_filter": { + "homes_select": { "title": "Selecionar novamente a Casa e os Dispositivos", "description": "## Instruções de Utilização\r\n### Modo de Controlo\r\n- Automático: Quando houver um gateway central Xiaomi disponível na rede local, o Home Assistant priorizará o envio de comandos através dele para obter controlo local. Se não existir um gateway central, tentará enviar comandos através da função de controlo LAN da Xiaomi. Apenas se estas condições não forem satisfeitas, os comandos serão enviados pela nuvem.\r\n- Nuvem: Todos os comandos de controlo são enviados através da nuvem.\r\n### Importar dispositivos da casa\r\nA integração adicionará dispositivos das casas selecionadas.\r\n \r\n### Olá {nick_name}, selecione o modo de controlo da integração e a casa da qual pretende importar dispositivos.", "data": { diff --git a/custom_components/xiaomi_home/translations/ru.json b/custom_components/xiaomi_home/translations/ru.json index 778206d..8418f93 100644 --- a/custom_components/xiaomi_home/translations/ru.json +++ b/custom_components/xiaomi_home/translations/ru.json @@ -22,7 +22,7 @@ "title": "Ошибка входа в систему", "description": "Нажмите кнопку «Далее», чтобы повторить попытку" }, - "devices_filter": { + "homes_select": { "title": "Выберите дом и устройства", "description": "## Инструкция по использованию\r\n### Режим управления\r\n- Авто: Когда в локальной сети доступен центральный шлюз Xiaomi, Home Assistant будет в первую очередь отправлять команды управления устройствами через центральный шлюз для достижения локализованного управления. Если в локальной сети нет центрального шлюза, он попытается отправить команды управления через протокол Xiaomi OT для достижения локализованного управления. Только если вышеуказанные условия локализованного управления не выполняются, команды управления устройствами будут отправляться через облако.\r\n- Облако: Команды управления отправляются только через облако.\r\n### Импорт домашнего устройства\r\nИнтеграция добавит устройства из выбранных домов.\r\n### Режим синхронизации имен комнат\r\nПри синхронизации устройств из приложения Xiaomi Home в Home Assistant имена комнат устройств в Home Assistant будут именоваться в соответствии с именами дома или комнаты в приложении Xiaomi Home.\r\n- Не синхронизировать: устройство не будет добавлено в любую область.\r\n- Другие параметры: область, в которую добавляется устройство, называется именем дома или комнаты в приложении Xiaomi Home.\r\n### Режим отладки Action\r\nДля методов, определенных в MIoT-Spec-V2, помимо создания уведомительной сущности будет создана сущность текстового поля ввода, которую можно использовать для отправки команд управления устройством во время отладки.\r\n### Скрыть нестандартные сущности\r\nСкрыть сущности, созданные нестандартными примерами MIoT-Spec-V2, имена которых начинаются с « * ».\r\n\r\n \r\n### {nick_name} Здравствуйте! Выберите режим управления интеграцией и дом, в котором находятся устройства, которые вы хотите добавить.", "data": { @@ -87,7 +87,7 @@ "nick_name": "Имя пользователя" } }, - "devices_filter": { + "homes_select": { "title": "Выберите дом и устройства", "description": "## Инструкция по использованию\r\n### Режим управления\r\n- Авто: Когда в локальной сети доступен центральный шлюз Xiaomi, Home Assistant будет в первую очередь отправлять команды управления устройствами через центральный шлюз для достижения локализованного управления. Если в локальной сети нет центрального шлюза, он попытается отправить команды управления через протокол Xiaomi OT для достижения локализованного управления. Только если вышеуказанные условия локализованного управления не выполняются, команды управления устройствами будут отправляться через облако.\r\n- Облако: Команды управления отправляются только через облако.\r\n### Импорт домашнего устройства\r\nИнтеграция добавит устройства из выбранных домов.\r\n \r\n### {nick_name} Здравствуйте! Выберите режим управления интеграцией и дом, в котором находятся устройства, которые вы хотите добавить.", "data": { diff --git a/custom_components/xiaomi_home/translations/zh-Hans.json b/custom_components/xiaomi_home/translations/zh-Hans.json index 8326832..37b905d 100644 --- a/custom_components/xiaomi_home/translations/zh-Hans.json +++ b/custom_components/xiaomi_home/translations/zh-Hans.json @@ -22,7 +22,7 @@ "title": "登录出现错误", "description": "点击“下一步”重试" }, - "devices_filter": { + "homes_select": { "title": "选择家庭与设备", "description": "## 使用介绍\r\n### 控制模式\r\n- 自动:本地局域网内存在可用的小米中枢网关时, Home Assistant 会优先通过中枢网关发送设备控制指令,以实现本地化控制功能。本地局域网不存在中枢时,会尝试通过小米OT协议发送控制指令,以实现本地化控制功能。只有当上述本地化控制条件不满足时,设备控制指令才会通过云端发送。\r\n- 云端:控制指令仅通过云端发送。\r\n### 导入设备的家庭\r\n集成将添加已选中家庭中的设备。\r\n### 房间名同步模式\r\n将设备从米家APP同步到 Home Assistant 时,设备在 Home Assistant 中所处区域的名称的命名方式将遵循以下规则。注意,设备同步过程不会改变米家APP中家庭和房间的设置。\r\n- 不同步:设备不会被添加至任何区域。\r\n- 其它选项:设备所添加到的区域以米家APP中的家庭或房间名称命名。\r\n### Action 调试模式\r\n对于设备 MIoT-Spec-V2 定义的方法,在生成通知实体之外,还会生成一个文本输入框实体,您可以在调试时用它向设备发送控制指令。\r\n### 隐藏非标准生成实体\r\n隐藏名称以“*”开头的非标准 MIoT-Spec-V2 实例生成的实体。\r\n\r\n \r\n### {nick_name} 您好!请选择集成控制模式以及您想要添加的设备所处的家庭。", "data": { @@ -87,7 +87,7 @@ "nick_name": "用户昵称" } }, - "devices_filter": { + "homes_select": { "title": "重新选择家庭与设备", "description": "## 使用介绍\r\n### 控制模式\r\n- 自动:本地局域网内存在可用的小米中枢网关时, Home Assistant 会优先通过中枢网关发送设备控制指令,以实现本地化控制功能。本地局域网不存在中枢时,会尝试通过小米OT协议发送控制指令,以实现本地化控制功能。只有当上述本地化控制条件不满足时,设备控制指令才会通过云端发送。\r\n- 云端:控制指令仅通过云端发送。\r\n### 导入设备的家庭\r\n集成将添加已选中家庭中的设备。\r\n \r\n### {nick_name} 您好!请选择集成控制模式以及您想要添加的设备所处的家庭。", "data": { diff --git a/custom_components/xiaomi_home/translations/zh-Hant.json b/custom_components/xiaomi_home/translations/zh-Hant.json index 04823c1..c8551a0 100644 --- a/custom_components/xiaomi_home/translations/zh-Hant.json +++ b/custom_components/xiaomi_home/translations/zh-Hant.json @@ -22,7 +22,7 @@ "title": "登錄出現錯誤", "description": "點擊“下一步”重試" }, - "devices_filter": { + "homes_select": { "title": "選擇家庭與設備", "description": "## 使用介紹\r\n### 控制模式\r\n- 自動:本地區域網內存在可用的小米中樞網關時, Home Assistant 會優先通過中樞網關發送設備控制指令,以實現本地化控制功能。本地區域網不存在中樞時,會嘗試通過小米OT協議發送控制指令,以實現本地化控制功能。只有當上述本地化控制條件不滿足時,設備控制指令才會通過雲端發送。\r\n- 雲端:控制指令僅通過雲端發送。\r\n### 導入設備的家庭\r\n集成將添加已選中家庭中的設備。\r\n### 房間名同步模式\r\n將設備從米家APP同步到 Home Assistant 時,設備在 Home Assistant 中所處區域的名稱的命名方式將遵循以下規則。注意,設備同步過程不會改變米家APP中家庭和房間的設置。\r\n- 不同步:設備不會被添加至任何區域。\r\n- 其它選項:設備所添加到的區域以米家APP中的家庭或房間名稱命名。\r\n### Action 調試模式\r\n對於設備 MIoT-Spec-V2 定義的方法,在生成通知實體之外,還會生成一個文本輸入框實體,您可以在調試時用它向設備發送控制指令。\r\n### 隱藏非標準生成實體\r\n隱藏名稱以“*”開頭的非標準 MIoT-Spec-V2 實例生成的實體。\r\n\r\n \r\n### {nick_name} 您好!請選擇集成控制模式以及您想要添加的設備所處的家庭。", "data": { @@ -87,7 +87,7 @@ "nick_name": "用戶暱稱" } }, - "devices_filter": { + "homes_select": { "title": "重新選擇家庭與設備", "description": "\r\n## 使用介紹\r\n### 控制模式\r\n- 自動:本地局域網內存在可用的小米中樞網關時, Home Assistant 會優先通過中樞網關發送設備控制指令,以實現本地化控制功能。只有當本地化控制條件不滿足時,設備控制指令才會通過雲端發送。\r\n- 雲端:控制指令強制通過雲端發送。\r\n### 導入設備的家庭\r\n集成將添加已選中家庭中的設備。\r\n \r\n### {nick_name} 您好!請選擇集成控制模式以及您想要添加的設備所處的家庭。", "data": { From afef70983953c3caf597747ff80b1b824b673c97 Mon Sep 17 00:00:00 2001 From: Paul Shawn <32349595+topsworld@users.noreply.github.com> Date: Sat, 21 Dec 2024 11:41:24 +0800 Subject: [PATCH 43/44] doc: update changelog and version to v0.1.4b0 (#312) --- CHANGELOG.md | 7 +++++++ custom_components/xiaomi_home/manifest.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1fdf52..50dfdee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## v0.1.4b0 +### Added +### Changed +### Fixed +- Fix miot cloud token refresh logic. [#307](https://github.com/XiaoMi/ha_xiaomi_home/pull/307) +- Fix lan ctrl filter logic. [#303](https://github.com/XiaoMi/ha_xiaomi_home/pull/303) + ## v0.1.3 ### Added ### Changed diff --git a/custom_components/xiaomi_home/manifest.json b/custom_components/xiaomi_home/manifest.json index 19afdc2..b5994a7 100644 --- a/custom_components/xiaomi_home/manifest.json +++ b/custom_components/xiaomi_home/manifest.json @@ -26,7 +26,7 @@ "psutil", "aiohttp[speedups]" ], - "version": "v0.1.3", + "version": "v0.1.4b0", "zeroconf": [ "_miot-central._tcp.local." ] From c1867e2baf4946f4666b7e4057509d2f615d3012 Mon Sep 17 00:00:00 2001 From: Paul Shawn <32349595+topsworld@users.noreply.github.com> Date: Sun, 22 Dec 2024 10:46:58 +0800 Subject: [PATCH 44/44] fix: fix type error, wrong use of any and Any (#338) * fix: fix type error, wrong use of any and Any * fix: wrong use of session close * fix: fix test_lan type error * fix: remove __del__ * feat: oauth, http add deinit_async --- custom_components/xiaomi_home/climate.py | 4 +- custom_components/xiaomi_home/config_flow.py | 20 ++- custom_components/xiaomi_home/event.py | 3 +- custom_components/xiaomi_home/fan.py | 2 +- custom_components/xiaomi_home/humidifier.py | 4 +- custom_components/xiaomi_home/light.py | 4 +- custom_components/xiaomi_home/miot/common.py | 8 +- .../xiaomi_home/miot/miot_client.py | 81 +++++++++--- .../xiaomi_home/miot/miot_cloud.py | 22 +-- .../xiaomi_home/miot/miot_device.py | 60 ++++----- .../xiaomi_home/miot/miot_error.py | 5 +- custom_components/xiaomi_home/miot/miot_ev.py | 38 +++--- .../xiaomi_home/miot/miot_mips.py | 125 +++++++++--------- .../xiaomi_home/miot/miot_spec.py | 10 +- .../xiaomi_home/miot/miot_storage.py | 12 +- custom_components/xiaomi_home/sensor.py | 3 +- custom_components/xiaomi_home/water_heater.py | 6 +- test/test_lan.py | 3 +- 18 files changed, 237 insertions(+), 173 deletions(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index 106abb9..bd4cfe3 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -47,7 +47,7 @@ """ from __future__ import annotations import logging -from typing import Optional +from typing import Any, Optional from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -415,7 +415,7 @@ def swing_mode(self) -> Optional[str]: return SWING_OFF return None - def __ac_state_changed(self, prop: MIoTSpecProperty, value: any) -> None: + def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None: del prop if not isinstance(value, str): _LOGGER.error( diff --git a/custom_components/xiaomi_home/config_flow.py b/custom_components/xiaomi_home/config_flow.py index b9f3776..36f9815 100644 --- a/custom_components/xiaomi_home/config_flow.py +++ b/custom_components/xiaomi_home/config_flow.py @@ -315,6 +315,9 @@ async def async_step_oauth(self, user_input=None): _LOGGER.error('task_oauth exception, %s', error) self._config_error_reason = str(error) return self.async_show_progress_done(next_step_id='oauth_error') + if self._miot_oauth: + await self._miot_oauth.deinit_async() + self._miot_oauth = None return self.async_show_progress_done(next_step_id='homes_select') return self.async_show_progress( step_id='oauth', @@ -336,10 +339,16 @@ async def __check_oauth_async(self) -> None: try: auth_info = await self._miot_oauth.get_access_token_async( code=oauth_code) - self._miot_http = MIoTHttpClient( - cloud_server=self._cloud_server, - client_id=OAUTH2_CLIENT_ID, - access_token=auth_info['access_token']) + if not self._miot_http: + self._miot_http = MIoTHttpClient( + cloud_server=self._cloud_server, + client_id=OAUTH2_CLIENT_ID, + access_token=auth_info['access_token']) + else: + self._miot_http.update_http_header( + cloud_server=self._cloud_server, + client_id=OAUTH2_CLIENT_ID, + access_token=auth_info['access_token']) self._auth_info = auth_info # Gen uuid self._uuid = hashlib.sha256( @@ -449,6 +458,9 @@ async def __check_oauth_async(self) -> None: # Auth success, unregister oauth webhook webhook_async_unregister(self.hass, webhook_id=self._virtual_did) + if self._miot_http: + await self._miot_http.deinit_async() + self._miot_http = None _LOGGER.info( '__check_oauth_async, webhook.async_unregister: %s', self._virtual_did) diff --git a/custom_components/xiaomi_home/event.py b/custom_components/xiaomi_home/event.py index 9b12526..7892290 100644 --- a/custom_components/xiaomi_home/event.py +++ b/custom_components/xiaomi_home/event.py @@ -46,6 +46,7 @@ Event entities for Xiaomi Home. """ from __future__ import annotations +from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -84,6 +85,6 @@ def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecEvent) -> None: # Set device_class self._attr_device_class = spec.device_class - def on_event_occurred(self, name: str, arguments: list[dict[int, any]]): + def on_event_occurred(self, name: str, arguments: list[dict[int, Any]]): """An event is occurred.""" self._trigger_event(event_type=name, event_attributes=arguments) diff --git a/custom_components/xiaomi_home/fan.py b/custom_components/xiaomi_home/fan.py index adae99a..42947ce 100644 --- a/custom_components/xiaomi_home/fan.py +++ b/custom_components/xiaomi_home/fan.py @@ -93,7 +93,7 @@ class Fan(MIoTServiceEntity, FanEntity): _speed_min: Optional[int] _speed_max: Optional[int] _speed_step: Optional[int] - _mode_list: Optional[dict[any, any]] + _mode_list: Optional[dict[Any, Any]] def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData diff --git a/custom_components/xiaomi_home/humidifier.py b/custom_components/xiaomi_home/humidifier.py index 62cd9cd..9739da4 100644 --- a/custom_components/xiaomi_home/humidifier.py +++ b/custom_components/xiaomi_home/humidifier.py @@ -47,7 +47,7 @@ """ from __future__ import annotations import logging -from typing import Optional +from typing import Any, Optional from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -97,7 +97,7 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity): _prop_target_humidity: Optional[MIoTSpecProperty] _prop_humidity: Optional[MIoTSpecProperty] - _mode_list: dict[any, any] + _mode_list: dict[Any, Any] def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData diff --git a/custom_components/xiaomi_home/light.py b/custom_components/xiaomi_home/light.py index 49229f5..bcfef6c 100644 --- a/custom_components/xiaomi_home/light.py +++ b/custom_components/xiaomi_home/light.py @@ -47,7 +47,7 @@ """ from __future__ import annotations import logging -from typing import Optional +from typing import Any, Optional from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -102,7 +102,7 @@ class Light(MIoTServiceEntity, LightEntity): _prop_mode: Optional[MIoTSpecProperty] _brightness_scale: Optional[tuple[int, int]] - _mode_list: Optional[dict[any, any]] + _mode_list: Optional[dict[Any, Any]] def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData diff --git a/custom_components/xiaomi_home/miot/common.py b/custom_components/xiaomi_home/miot/common.py index 0b03f96..714f158 100644 --- a/custom_components/xiaomi_home/miot/common.py +++ b/custom_components/xiaomi_home/miot/common.py @@ -48,9 +48,9 @@ import json from os import path import random -from typing import Optional +from typing import Any, Optional import hashlib -from paho.mqtt.client import MQTTMatcher +from paho.mqtt.matcher import MQTTMatcher import yaml MIOT_ROOT_PATH: str = path.dirname(path.abspath(__file__)) @@ -87,7 +87,7 @@ def randomize_int(value: int, ratio: float) -> int: class MIoTMatcher(MQTTMatcher): """MIoT Pub/Sub topic matcher.""" - def iter_all_nodes(self) -> any: + def iter_all_nodes(self) -> Any: """Return an iterator on all nodes with their paths and contents.""" def rec(node, path_): # pylint: disable=protected-access @@ -97,7 +97,7 @@ def rec(node, path_): yield from rec(child, path_ + [part]) return rec(self._root, []) - def get(self, topic: str) -> Optional[any]: + def get(self, topic: str) -> Optional[Any]: try: return self[topic] except KeyError: diff --git a/custom_components/xiaomi_home/miot/miot_client.py b/custom_components/xiaomi_home/miot/miot_client.py index 7423e20..da8e2b6 100644 --- a/custom_components/xiaomi_home/miot/miot_client.py +++ b/custom_components/xiaomi_home/miot/miot_client.py @@ -1,7 +1,52 @@ # -*- coding: utf-8 -*- -"""MIoT client instance.""" +""" +Copyright (C) 2024 Xiaomi Corporation. + +The ownership and intellectual property rights of Xiaomi Home Assistant +Integration and related Xiaomi cloud service API interface provided under this +license, including source code and object code (collectively, "Licensed Work"), +are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi +hereby grants you a personal, limited, non-exclusive, non-transferable, +non-sublicensable, and royalty-free license to reproduce, use, modify, and +distribute the Licensed Work only for your use of Home Assistant for +non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize +you to use the Licensed Work for any other purpose, including but not limited +to use Licensed Work to develop applications (APP), Web services, and other +forms of software. + +You may reproduce and distribute copies of the Licensed Work, with or without +modifications, whether in source or object form, provided that you must give +any other recipients of the Licensed Work a copy of this License and retain all +copyright and disclaimers. + +Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied, including, without +limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR +OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or +FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible +for any direct, indirect, special, incidental, or consequential damages or +losses arising from the use or inability to use the Licensed Work. + +Xiaomi reserves all rights not expressly granted to you in this License. +Except for the rights expressly granted by Xiaomi under this License, Xiaomi +does not authorize you in any form to use the trademarks, copyrights, or other +forms of intellectual property rights of Xiaomi and its affiliates, including, +without limitation, without obtaining other written permission from Xiaomi, you +shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that +may make the public associate with Xiaomi in any form to publicize or promote +the software or hardware devices that use the Licensed Work. + +Xiaomi has the right to immediately terminate all your authorization under this +License in the event: +1. You assert patent invalidation, litigation, or other claims against patents +or other intellectual property rights of Xiaomi or its affiliates; or, +2. You make, have made, manufacture, sell, or offer to sell products that knock +off Xiaomi or its affiliates' products. + +MIoT client instance. +""" from copy import deepcopy -from typing import Callable, Optional, final +from typing import Any, Callable, Optional, final import asyncio import json import logging @@ -36,9 +81,9 @@ @dataclass class MIoTClientSub: """MIoT client subscription.""" - topic: str = None - handler: Callable[[dict, any], None] = None - handler_ctx: any = None + topic: Optional[str] + handler: Callable[[dict, Any], None] + handler_ctx: Any = None def __str__(self) -> str: return f'{self.topic}, {id(self.handler)}, {id(self.handler_ctx)}' @@ -345,6 +390,8 @@ async def deinit_async(self) -> None: if self._show_devices_changed_notify_timer: self._show_devices_changed_notify_timer.cancel() self._show_devices_changed_notify_timer = None + await self._oauth.deinit_async() + await self._http.deinit_async() # Remove notify self._persistence_notify( self.__gen_notify_key('dev_list_changed'), None, None) @@ -526,7 +573,7 @@ async def refresh_user_cert_async(self) -> bool: return False async def set_prop_async( - self, did: str, siid: int, piid: int, value: any + 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}') @@ -612,7 +659,7 @@ def request_refresh_prop( 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: + async def get_prop_async(self, did: str, siid: int, piid: int) -> Any: if did not in self._device_list_cache: raise MIoTClientError(f'did not exist, {did}') @@ -717,8 +764,8 @@ async def action_async( return None def sub_prop( - self, did: str, handler: Callable[[dict, any], None], - siid: int = None, piid: int = None, handler_ctx: any = None + self, did: str, handler: Callable[[dict, Any], None], + siid: int = None, piid: int = None, handler_ctx: Any = None ) -> bool: if did not in self._device_list_cache: raise MIoTClientError(f'did not exist, {did}') @@ -741,8 +788,8 @@ def unsub_prop(self, did: str, siid: int = None, piid: int = None) -> bool: return True def sub_event( - self, did: str, handler: Callable[[dict, any], None], - siid: int = None, eiid: int = None, handler_ctx: any = None + self, did: str, handler: Callable[[dict, Any], None], + siid: int = None, eiid: int = None, handler_ctx: Any = None ) -> bool: if did not in self._device_list_cache: raise MIoTClientError(f'did not exist, {did}') @@ -764,8 +811,8 @@ def unsub_event(self, did: str, siid: int = None, eiid: int = None) -> bool: return True def sub_device_state( - self, did: str, handler: Callable[[str, MIoTDeviceState, any], None], - handler_ctx: any = None + self, did: str, handler: Callable[[str, MIoTDeviceState, Any], None], + handler_ctx: Any = None ) -> bool: """Call callback handler in main loop""" if did not in self._device_list_cache: @@ -1060,7 +1107,7 @@ async def __on_miot_lan_state_change(self, state: bool) -> None: @final def __on_cloud_device_state_changed( - self, did: str, state: MIoTDeviceState, ctx: any + 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) @@ -1110,7 +1157,7 @@ async def __on_gw_device_list_changed( @final async def __on_lan_device_state_changed( - self, did: str, state: dict, ctx: any + 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) @@ -1146,7 +1193,7 @@ async def __on_lan_device_state_changed( self.__request_show_devices_changed_notify() @final - def __on_prop_msg(self, params: dict, ctx: any) -> None: + def __on_prop_msg(self, params: dict, ctx: Any) -> None: """params MUST contain did, siid, piid, value""" # BLE device has no online/offline msg try: @@ -1158,7 +1205,7 @@ def __on_prop_msg(self, params: dict, ctx: any) -> None: _LOGGER.error('on prop msg error, %s, %s', params, err) @final - def __on_event_msg(self, params: dict, ctx: any) -> None: + def __on_event_msg(self, params: dict, ctx: Any) -> None: try: subs: list[MIoTClientSub] = list(self._sub_tree.iter_match( f'{params["did"]}/e/{params["siid"]}/{params["eiid"]}')) diff --git a/custom_components/xiaomi_home/miot/miot_cloud.py b/custom_components/xiaomi_home/miot/miot_cloud.py index 2d5c49c..036ea57 100644 --- a/custom_components/xiaomi_home/miot/miot_cloud.py +++ b/custom_components/xiaomi_home/miot/miot_cloud.py @@ -51,7 +51,7 @@ import logging import re import time -from typing import Optional +from typing import Any, Optional from urllib.parse import urlencode import aiohttp @@ -94,10 +94,11 @@ def __init__( self._oauth_host = DEFAULT_OAUTH2_API_HOST else: self._oauth_host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}' - self._session = aiohttp.ClientSession() + self._session = aiohttp.ClientSession(loop=self._main_loop) - def __del__(self): - self._session.close() + async def deinit_async(self) -> None: + if self._session and not self._session.closed: + await self._session.close() def set_redirect_url(self, redirect_url: str) -> None: if not isinstance(redirect_url, str) or redirect_url.strip() == '': @@ -250,10 +251,11 @@ def __init__( cloud_server=cloud_server, client_id=client_id, access_token=access_token) - self._session = aiohttp.ClientSession() + self._session = aiohttp.ClientSession(loop=self._main_loop) - def __del__(self): - self._session.close() + async def deinit_async(self) -> None: + if self._session and not self._session.closed: + await self._session.close() def update_http_header( self, cloud_server: Optional[str] = None, @@ -581,7 +583,7 @@ async def get_devices_async( self, home_ids: list[str] = None ) -> dict[str, dict]: homeinfos = await self.get_homeinfos_async() - homes: dict[str, dict[str, any]] = {} + homes: dict[str, dict[str, Any]] = {} devices: dict[str, dict] = {} for device_type in ['home_list', 'share_home_list']: homes.setdefault(device_type, {}) @@ -661,7 +663,7 @@ async def get_props_async(self, params: list) -> list: raise MIoTHttpError('invalid response result') return res_obj['result'] - async def __get_prop_async(self, did: str, siid: int, piid: int) -> any: + 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}]) if not results: @@ -722,7 +724,7 @@ async def __get_prop_handler(self) -> bool: async def get_prop_async( self, did: str, siid: int, piid: int, immediately: bool = False - ) -> any: + ) -> Any: if immediately: return await self.__get_prop_async(did, siid, piid) key: str = f'{did}.{siid}.{piid}' diff --git a/custom_components/xiaomi_home/miot/miot_device.py b/custom_components/xiaomi_home/miot/miot_device.py index fa48345..6144d57 100644 --- a/custom_components/xiaomi_home/miot/miot_device.py +++ b/custom_components/xiaomi_home/miot/miot_device.py @@ -47,7 +47,7 @@ """ import asyncio from abc import abstractmethod -from typing import Callable, Optional +from typing import Any, Callable, Optional import logging from homeassistant.helpers.entity import Entity @@ -103,7 +103,7 @@ class MIoTEntityData: """MIoT Entity Data.""" platform: str - device_class: any + device_class: Any spec: MIoTSpecInstance | MIoTSpecService props: set[MIoTSpecProperty] @@ -243,8 +243,8 @@ def unsub_device_state(self, key: str) -> bool: return True def sub_property( - self, handler: Callable[[dict, any], None], siid: int = None, - piid: int = None, handler_ctx: any = None + self, handler: Callable[[dict, Any], None], siid: int = None, + piid: int = None, handler_ctx: Any = None ) -> bool: return self.miot_client.sub_prop( did=self._did, handler=handler, siid=siid, piid=piid, @@ -254,8 +254,8 @@ def unsub_property(self, siid: int = None, piid: int = None) -> bool: return self.miot_client.unsub_prop(did=self._did, siid=siid, piid=piid) def sub_event( - self, handler: Callable[[dict, any], None], siid: int = None, - eiid: int = None, handler_ctx: any = None + self, handler: Callable[[dict, Any], None], siid: int = None, + eiid: int = None, handler_ctx: Any = None ) -> bool: return self.miot_client.sub_event( did=self._did, handler=handler, siid=siid, eiid=eiid, @@ -688,7 +688,7 @@ def icon_convert(self, spec_unit: str) -> Optional[str]: return None def __on_device_state_changed( - self, did: str, state: MIoTDeviceState, ctx: any + self, did: str, state: MIoTDeviceState, ctx: Any ) -> None: self._online = state for key, handler in self._device_state_sub_list.items(): @@ -704,11 +704,11 @@ class MIoTServiceEntity(Entity): entity_data: MIoTEntityData _main_loop: asyncio.AbstractEventLoop - _prop_value_map: dict[MIoTSpecProperty, any] + _prop_value_map: dict[MIoTSpecProperty, Any] _event_occurred_handler: Callable[[MIoTSpecEvent, dict], None] _prop_changed_subs: dict[ - MIoTSpecProperty, Callable[[MIoTSpecProperty, any], None]] + MIoTSpecProperty, Callable[[MIoTSpecProperty, Any], None]] _pending_write_ha_state_timer: Optional[asyncio.TimerHandle] @@ -759,7 +759,7 @@ def event_occurred_handler(self, func) -> None: def sub_prop_changed( self, prop: MIoTSpecProperty, - handler: Callable[[MIoTSpecProperty, any], None] + handler: Callable[[MIoTSpecProperty, Any], None] ) -> None: if not prop or not handler: _LOGGER.error( @@ -816,13 +816,13 @@ async def async_will_remove_from_hass(self) -> None: self.miot_device.unsub_event( siid=event.service.iid, eiid=event.iid) - def get_map_description(self, map_: dict[int, any], key: int) -> any: + def get_map_description(self, map_: dict[int, Any], key: int) -> Any: if map_ is None: return None return map_.get(key, None) def get_map_value( - self, map_: dict[int, any], description: any + self, map_: dict[int, Any], description: Any ) -> Optional[int]: if map_ is None: return None @@ -831,7 +831,7 @@ def get_map_value( return key return None - def get_prop_value(self, prop: MIoTSpecProperty) -> any: + def get_prop_value(self, prop: MIoTSpecProperty) -> Any: if not prop: _LOGGER.error( 'get_prop_value error, property is None, %s, %s', @@ -839,7 +839,7 @@ def get_prop_value(self, prop: MIoTSpecProperty) -> any: return None return self._prop_value_map.get(prop, None) - def set_prop_value(self, prop: MIoTSpecProperty, value: any) -> None: + def set_prop_value(self, prop: MIoTSpecProperty, value: Any) -> None: if not prop: _LOGGER.error( 'set_prop_value error, property is None, %s, %s', @@ -848,7 +848,7 @@ def set_prop_value(self, prop: MIoTSpecProperty, value: any) -> None: self._prop_value_map[prop] = value async def set_property_async( - self, prop: MIoTSpecProperty, value: any, update: bool = True + self, prop: MIoTSpecProperty, value: Any, update: bool = True ) -> bool: value = prop.value_format(value) if not prop: @@ -875,7 +875,7 @@ async def set_property_async( self.async_write_ha_state() return True - async def get_property_async(self, prop: MIoTSpecProperty) -> any: + async def get_property_async(self, prop: MIoTSpecProperty) -> Any: if not prop: _LOGGER.error( 'get property failed, property is None, %s, %s', @@ -914,7 +914,7 @@ async def action_async( f'{e}, {self.entity_id}, {self.name}, {action.name}') from e return True - def __on_properties_changed(self, params: dict, ctx: any) -> None: + def __on_properties_changed(self, params: dict, ctx: Any) -> None: _LOGGER.debug('properties changed, %s', params) for prop in self.entity_data.props: if ( @@ -922,7 +922,7 @@ def __on_properties_changed(self, params: dict, ctx: any) -> None: or prop.service.iid != params['siid'] ): continue - value: any = prop.value_format(params['value']) + value: Any = prop.value_format(params['value']) self._prop_value_map[prop] = value if prop in self._prop_changed_subs: self._prop_changed_subs[prop](prop, value) @@ -930,7 +930,7 @@ def __on_properties_changed(self, params: dict, ctx: any) -> None: if not self._pending_write_ha_state_timer: self.async_write_ha_state() - def __on_event_occurred(self, params: dict, ctx: any) -> None: + def __on_event_occurred(self, params: dict, ctx: Any) -> None: _LOGGER.debug('event occurred, %s', params) if self._event_occurred_handler is None: return @@ -988,9 +988,9 @@ class MIoTPropertyEntity(Entity): _main_loop: asyncio.AbstractEventLoop # {'min':int, 'max':int, 'step': int} _value_range: dict[str, int] - # {any: any} - _value_list: dict[any, any] - _value: any + # {Any: Any} + _value_list: dict[Any, Any] + _value: Any _pending_write_ha_state_timer: Optional[asyncio.TimerHandle] @@ -1054,12 +1054,12 @@ async def async_will_remove_from_hass(self) -> None: self.miot_device.unsub_property( siid=self.service.iid, piid=self.spec.iid) - def get_vlist_description(self, value: any) -> str: + def get_vlist_description(self, value: Any) -> str: if not self._value_list: return None return self._value_list.get(value, None) - def get_vlist_value(self, description: str) -> any: + def get_vlist_value(self, description: str) -> Any: if not self._value_list: return None for key, value in self._value_list.items(): @@ -1067,7 +1067,7 @@ def get_vlist_value(self, description: str) -> any: return key return None - async def set_property_async(self, value: any) -> bool: + async def set_property_async(self, value: Any) -> bool: if not self.spec.writable: raise RuntimeError( f'set property failed, not writable, ' @@ -1084,7 +1084,7 @@ async def set_property_async(self, value: any) -> bool: self.async_write_ha_state() return True - async def get_property_async(self) -> any: + async def get_property_async(self) -> Any: if not self.spec.readable: _LOGGER.error( 'get property failed, not readable, %s, %s', @@ -1095,7 +1095,7 @@ async def get_property_async(self) -> any: did=self.miot_device.did, siid=self.spec.service.iid, piid=self.spec.iid)) - def __on_value_changed(self, params: dict, ctx: any) -> None: + def __on_value_changed(self, params: dict, ctx: Any) -> None: _LOGGER.debug('property changed, %s', params) self._value = self.spec.value_format(params['value']) if not self._pending_write_ha_state_timer: @@ -1135,7 +1135,7 @@ class MIoTEventEntity(Entity): service: MIoTSpecService _main_loop: asyncio.AbstractEventLoop - _value: any + _value: Any _attr_event_types: list[str] _arguments_map: dict[int, str] @@ -1192,10 +1192,10 @@ async def async_will_remove_from_hass(self) -> None: @abstractmethod def on_event_occurred( - self, name: str, arguments: list[dict[int, any]] + self, name: str, arguments: list[dict[int, Any]] ): ... - def __on_event_occurred(self, params: dict, ctx: any) -> None: + def __on_event_occurred(self, params: dict, ctx: Any) -> None: _LOGGER.debug('event occurred, %s', params) trans_arg = {} try: diff --git a/custom_components/xiaomi_home/miot/miot_error.py b/custom_components/xiaomi_home/miot/miot_error.py index 1004a15..a85bfd4 100644 --- a/custom_components/xiaomi_home/miot/miot_error.py +++ b/custom_components/xiaomi_home/miot/miot_error.py @@ -46,6 +46,7 @@ MIoT error code and exception. """ from enum import Enum +from typing import Any class MIoTErrorCode(Enum): @@ -78,10 +79,10 @@ class MIoTErrorCode(Enum): class MIoTError(Exception): """MIoT error.""" code: MIoTErrorCode - message: any + message: Any def __init__( - self, message: any, code: MIoTErrorCode = MIoTErrorCode.CODE_UNKNOWN + self, message: Any, code: MIoTErrorCode = MIoTErrorCode.CODE_UNKNOWN ) -> None: self.message = message self.code = code diff --git a/custom_components/xiaomi_home/miot/miot_ev.py b/custom_components/xiaomi_home/miot/miot_ev.py index be4e684..c0cc97f 100644 --- a/custom_components/xiaomi_home/miot/miot_ev.py +++ b/custom_components/xiaomi_home/miot/miot_ev.py @@ -49,7 +49,7 @@ import heapq import time import traceback -from typing import Callable, TypeVar +from typing import Any, Callable, TypeVar import logging import threading @@ -64,17 +64,17 @@ class MIoTFdHandler: """File descriptor handler.""" fd: int - read_handler: Callable[[any], None] - read_handler_ctx: any - write_handler: Callable[[any], None] - write_handler_ctx: any + read_handler: Callable[[Any], None] + read_handler_ctx: Any + write_handler: Callable[[Any], None] + write_handler_ctx: Any def __init__( self, fd: int, - read_handler: Callable[[any], None] = None, - read_handler_ctx: any = None, - write_handler: Callable[[any], None] = None, - write_handler_ctx: any = None + read_handler: Callable[[Any], None] = None, + read_handler_ctx: Any = None, + write_handler: Callable[[Any], None] = None, + write_handler_ctx: Any = None ) -> None: self.fd = fd self.read_handler = read_handler @@ -87,13 +87,13 @@ class MIoTTimeout: """Timeout handler.""" key: TimeoutHandle target: int - handler: Callable[[any], None] - handler_ctx: any + handler: Callable[[Any], None] + handler_ctx: Any def __init__( self, key: str = None, target: int = None, - handler: Callable[[any], None] = None, - handler_ctx: any = None + handler: Callable[[Any], None] = None, + handler_ctx: Any = None ) -> None: self.key = key self.target = target @@ -185,8 +185,8 @@ def loop_stop(self) -> None: self._timer_handlers = {} def set_timeout( - self, timeout_ms: int, handler: Callable[[any], None], - handler_ctx: any = None + self, timeout_ms: int, handler: Callable[[Any], None], + handler_ctx: Any = None ) -> TimeoutHandle: """Set a timer.""" if timeout_ms is None or handler is None: @@ -211,7 +211,7 @@ def clear_timeout(self, timer_key: TimeoutHandle) -> None: heapq.heapify(self._timer_heap) def set_read_handler( - self, fd: int, handler: Callable[[any], None], handler_ctx: any = None + self, fd: int, handler: Callable[[Any], None], handler_ctx: Any = None ) -> bool: """Set a read handler for a file descriptor. @@ -222,7 +222,7 @@ def set_read_handler( fd, is_read=True, handler=handler, handler_ctx=handler_ctx) def set_write_handler( - self, fd: int, handler: Callable[[any], None], handler_ctx: any = None + self, fd: int, handler: Callable[[Any], None], handler_ctx: Any = None ) -> bool: """Set a write handler for a file descriptor. @@ -233,8 +233,8 @@ def set_write_handler( fd, is_read=False, handler=handler, handler_ctx=handler_ctx) def __set_handler( - self, fd, is_read: bool, handler: Callable[[any], None], - handler_ctx: any = None + self, fd, is_read: bool, handler: Callable[[Any], None], + handler_ctx: Any = None ) -> bool: """Set a handler.""" if fd is None: diff --git a/custom_components/xiaomi_home/miot/miot_mips.py b/custom_components/xiaomi_home/miot/miot_mips.py index cbe41cf..6c6b358 100644 --- a/custom_components/xiaomi_home/miot/miot_mips.py +++ b/custom_components/xiaomi_home/miot/miot_mips.py @@ -58,7 +58,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from enum import Enum, auto -from typing import Callable, Optional, final +from typing import Any, Callable, Optional, final from paho.mqtt.client import ( MQTT_ERR_SUCCESS, @@ -173,9 +173,9 @@ class MipsCmdType(Enum): class MipsCmd: """MIoT Pub/Sub command.""" type_: MipsCmdType - data: any + data: Any - def __init__(self, type_: MipsCmdType, data: any) -> None: + def __init__(self, type_: MipsCmdType, data: Any) -> None: self.type_ = type_ self.data = data @@ -184,8 +184,8 @@ def __init__(self, type_: MipsCmdType, data: any) -> None: class MipsRequest: """MIoT Pub/Sub request.""" mid: int = None - on_reply: Callable[[str, any], None] = None - on_reply_ctx: any = None + on_reply: Callable[[str, Any], None] = None + on_reply_ctx: Any = None timer: TimeoutHandle = None @@ -194,8 +194,8 @@ class MipsRequestData: """MIoT Pub/Sub request data.""" topic: str = None payload: str = None - on_reply: Callable[[str, any], None] = None - on_reply_ctx: any = None + on_reply: Callable[[str, Any], None] = None + on_reply_ctx: Any = None timeout_ms: int = None @@ -223,8 +223,8 @@ class MipsApi: param2: payload param3: handler_ctx """ - handler: Callable[[MipsIncomingApiCall, str, any], None] = None - handler_ctx: any = None + handler: Callable[[MipsIncomingApiCall, str, Any], None] = None + handler_ctx: Any = None class MipsRegApi(MipsApi): @@ -247,8 +247,8 @@ class MipsBroadcast: param 2: msg payload param 3: handle_ctx """ - handler: Callable[[str, str, any], None] = None - handler_ctx: any = None + handler: Callable[[str, str, Any], None] = None + handler_ctx: Any = None def __str__(self) -> str: return f'{self.topic}, {id(self.handler)}, {id(self.handler_ctx)}' @@ -265,7 +265,6 @@ class MipsState: """ str: key bool: mips connect state - any: ctx """ handler: Callable[[str, bool], asyncio.Future] = None @@ -288,10 +287,10 @@ class MipsDeviceState: """handler str: did MIoTDeviceState: online/offline/disable - any: ctx + Any: ctx """ - handler: Callable[[str, MIoTDeviceState, any], None] = None - handler_ctx: any = None + handler: Callable[[str, MIoTDeviceState, Any], None] = None + handler_ctx: Any = None class MipsRegDeviceState(MipsDeviceState): @@ -512,8 +511,8 @@ def unsub_mips_state(self, key: str) -> bool: @final def mev_set_timeout( - self, timeout_ms: int, handler: Callable[[any], None], - handler_ctx: any = None + self, timeout_ms: int, handler: Callable[[Any], None], + handler_ctx: Any = None ) -> Optional[TimeoutHandle]: """set timeout. NOTICE: Internal function, only mips threads are allowed to call @@ -534,7 +533,7 @@ def mev_clear_timeout(self, handle: TimeoutHandle) -> None: @final def mev_set_read_handler( - self, fd: int, handler: Callable[[any], None], handler_ctx: any + self, fd: int, handler: Callable[[Any], None], handler_ctx: Any ) -> bool: """set read handler. NOTICE: Internal function, only mips threads are allowed to call @@ -546,7 +545,7 @@ def mev_set_read_handler( @final def mev_set_write_handler( - self, fd: int, handler: Callable[[any], None], handler_ctx: any + self, fd: int, handler: Callable[[Any], None], handler_ctx: Any ) -> bool: """set write handler. NOTICE: Internal function, only mips threads are allowed to call @@ -604,8 +603,8 @@ def on_mips_disconnect(self, handler: Callable[[int, dict], None]) -> None: @abstractmethod def sub_prop( - self, did: str, handler: Callable[[dict, any], None], - siid: int = None, piid: int = None, handler_ctx: any = None + self, did: str, handler: Callable[[dict, Any], None], + siid: int = None, piid: int = None, handler_ctx: Any = None ) -> bool: ... @abstractmethod @@ -615,8 +614,8 @@ def unsub_prop( @abstractmethod def sub_event( - self, did: str, handler: Callable[[dict, any], None], - siid: int = None, eiid: int = None, handler_ctx: any = None + self, did: str, handler: Callable[[dict, Any], None], + siid: int = None, eiid: int = None, handler_ctx: Any = None ) -> bool: ... @abstractmethod @@ -632,11 +631,11 @@ async def get_dev_list_async( @abstractmethod async def get_prop_async( self, did: str, siid: int, piid: int, timeout_ms: int = 10000 - ) -> any: ... + ) -> Any: ... @abstractmethod async def set_prop_async( - self, did: str, siid: int, piid: int, value: any, + self, did: str, siid: int, piid: int, value: Any, timeout_ms: int = 10000 ) -> bool: ... @@ -709,7 +708,7 @@ def _mips_publish_internal( return False @final - def _mips_send_cmd(self, type_: MipsCmdType, data: any) -> bool: + def _mips_send_cmd(self, type_: MipsCmdType, data: Any) -> bool: if self._mips_queue is None or self._cmd_event_fd is None: raise MIoTMipsError('send mips cmd disable') # Put data to queue @@ -723,7 +722,7 @@ def __thread_check(self) -> None: if threading.current_thread() is not self._mips_thread: raise MIoTMipsError('illegal call') - def __mips_cmd_read_handler(self, ctx: any) -> None: + def __mips_cmd_read_handler(self, ctx: Any) -> None: fd_value = os.eventfd_read(self._cmd_event_fd) if fd_value == 0: return @@ -763,20 +762,20 @@ def __mips_cmd_read_handler(self, ctx: any) -> None: if self._on_mips_cmd: self._on_mips_cmd(mips_cmd=mips_cmd) - def __mqtt_read_handler(self, ctx: any) -> None: + def __mqtt_read_handler(self, ctx: Any) -> None: self.__mqtt_loop_handler(ctx=ctx) - def __mqtt_write_handler(self, ctx: any) -> None: + def __mqtt_write_handler(self, ctx: Any) -> None: self.mev_set_write_handler(self._mqtt_fd, None, None) self.__mqtt_loop_handler(ctx=ctx) - def __mqtt_timer_handler(self, ctx: any) -> None: + def __mqtt_timer_handler(self, ctx: Any) -> None: self.__mqtt_loop_handler(ctx=ctx) if self._mqtt: self._mqtt_timer = self.mev_set_timeout( self.MQTT_INTERVAL_MS, self.__mqtt_timer_handler, None) - def __mqtt_loop_handler(self, ctx: any) -> None: + def __mqtt_loop_handler(self, ctx: Any) -> None: try: if self._mqtt: self._mqtt.loop_read() @@ -896,7 +895,7 @@ def __mips_try_reconnect(self, immediately: bool = False) -> None: self._mips_reconnect_timer = self.mev_set_timeout( interval, self.__mips_connect, None) - def __mips_sub_internal_pending_handler(self, ctx: any) -> None: + def __mips_sub_internal_pending_handler(self, ctx: Any) -> None: subbed_count = 1 for topic in list(self._mips_sub_pending_map.keys()): if subbed_count > self.MIPS_SUB_PATCH: @@ -923,7 +922,7 @@ def __mips_sub_internal_pending_handler(self, ctx: any) -> None: else: self._mips_sub_pending_timer = None - def __mips_connect(self, ctx: any = None) -> None: + def __mips_connect(self, ctx: Any = None) -> None: result = MQTT_ERR_UNKNOWN if self._mips_reconnect_timer: self.mev_clear_timeout(self._mips_reconnect_timer) @@ -1034,8 +1033,8 @@ def update_access_token(self, access_token: str) -> bool: @final def sub_prop( - self, did: str, handler: Callable[[dict, any], None], - siid: int = None, piid: int = None, handler_ctx: any = None + self, did: str, handler: Callable[[dict, Any], None], + siid: int = None, piid: int = None, handler_ctx: Any = None ) -> bool: if not isinstance(did, str) or handler is None: raise MIoTMipsError('invalid params') @@ -1044,7 +1043,7 @@ def sub_prop( f'device/{did}/up/properties_changed/' f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}') - def on_prop_msg(topic: str, payload: str, ctx: any) -> bool: + def on_prop_msg(topic: str, payload: str, ctx: Any) -> bool: try: msg: dict = json.loads(payload) except json.JSONDecodeError: @@ -1077,8 +1076,8 @@ def unsub_prop(self, did: str, siid: int = None, piid: int = None) -> bool: @final def sub_event( - self, did: str, handler: Callable[[dict, any], None], - siid: int = None, eiid: int = None, handler_ctx: any = None + self, did: str, handler: Callable[[dict, Any], None], + siid: int = None, eiid: int = None, handler_ctx: Any = None ) -> bool: if not isinstance(did, str) or handler is None: raise MIoTMipsError('invalid params') @@ -1087,7 +1086,7 @@ def sub_event( f'device/{did}/up/event_occured/' f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}') - def on_event_msg(topic: str, payload: str, ctx: any) -> bool: + def on_event_msg(topic: str, payload: str, ctx: Any) -> bool: try: msg: dict = json.loads(payload) except json.JSONDecodeError: @@ -1122,15 +1121,15 @@ def unsub_event(self, did: str, siid: int = None, eiid: int = None) -> bool: @final def sub_device_state( - self, did: str, handler: Callable[[str, MIoTDeviceState, any], None], - handler_ctx: any = None + self, did: str, handler: Callable[[str, MIoTDeviceState, Any], None], + handler_ctx: Any = None ) -> bool: """subscribe online state.""" if not isinstance(did, str) or handler is None: raise MIoTMipsError('invalid params') topic: str = f'device/{did}/state/#' - def on_state_msg(topic: str, payload: str, ctx: any) -> None: + def on_state_msg(topic: str, payload: str, ctx: Any) -> None: msg: dict = json.loads(payload) # {"device_id":"xxxx","device_name":"米家智能插座3 ","event":"online", # "model": "cuco.plug.v3","timestamp":1709001070828,"uid":xxxx} @@ -1163,11 +1162,11 @@ async def get_dev_list_async( async def get_prop_async( self, did: str, siid: int, piid: int, timeout_ms: int = 10000 - ) -> any: + ) -> Any: raise NotImplementedError('please call in http client') async def set_prop_async( - self, did: str, siid: int, piid: int, value: any, + self, did: str, siid: int, piid: int, value: Any, timeout_ms: int = 10000 ) -> bool: raise NotImplementedError('please call in http client') @@ -1199,8 +1198,8 @@ def __on_mips_cmd_handler(self, mips_cmd: MipsCmd) -> None: self._mips_unsub_internal(topic=unreg_bc.topic) def __reg_broadcast( - self, topic: str, handler: Callable[[str, str, any], None], - handler_ctx: any = None + self, topic: str, handler: Callable[[str, str, Any], None], + handler_ctx: Any = None ) -> bool: return self._mips_send_cmd( type_=MipsCmdType.REG_BROADCAST, @@ -1259,7 +1258,7 @@ class MipsLocalClient(MipsClient): _device_state_sub_map: dict[str, MipsDeviceState] _get_prop_queue: dict[str, list] _get_prop_timer: asyncio.TimerHandle - _on_dev_list_changed: Callable[[any, list[str]], asyncio.Future] + _on_dev_list_changed: Callable[[Any, list[str]], asyncio.Future] def __init__( self, did: str, host: str, group_id: str, @@ -1347,14 +1346,14 @@ async def disconnect_async(self) -> None: @final def sub_prop( - self, did: str, handler: Callable[[dict, any], None], - siid: int = None, piid: int = None, handler_ctx: any = None + self, did: str, handler: Callable[[dict, Any], None], + siid: int = None, piid: int = None, handler_ctx: Any = None ) -> bool: topic: str = ( f'appMsg/notify/iot/{did}/property/' f'{"#" if siid is None or piid is None else f"{siid}.{piid}"}') - def on_prop_msg(topic: str, payload: str, ctx: any): + def on_prop_msg(topic: str, payload: str, ctx: Any): msg: dict = json.loads(payload) if ( msg is None @@ -1380,14 +1379,14 @@ def unsub_prop(self, did: str, siid: int = None, piid: int = None) -> bool: @final def sub_event( - self, did: str, handler: Callable[[dict, any], None], - siid: int = None, eiid: int = None, handler_ctx: any = None + self, did: str, handler: Callable[[dict, Any], None], + siid: int = None, eiid: int = None, handler_ctx: Any = None ) -> bool: topic: str = ( f'appMsg/notify/iot/{did}/event/' f'{"#" if siid is None or eiid is None else f"{siid}.{eiid}"}') - def on_event_msg(topic: str, payload: str, ctx: any): + def on_event_msg(topic: str, payload: str, ctx: Any): msg: dict = json.loads(payload) if ( msg is None @@ -1414,7 +1413,7 @@ def unsub_event(self, did: str, siid: int = None, eiid: int = None) -> bool: @final async def get_prop_safe_async( self, did: str, siid: int, piid: int, timeout_ms: int = 10000 - ) -> any: + ) -> Any: self._get_prop_queue.setdefault(did, []) fut: asyncio.Future = self.main_loop.create_future() self._get_prop_queue[did].append({ @@ -1434,7 +1433,7 @@ async def get_prop_safe_async( @final async def get_prop_async( self, did: str, siid: int, piid: int, timeout_ms: int = 10000 - ) -> any: + ) -> Any: result_obj = await self.__request_async( topic='proxy/get', payload=json.dumps({ @@ -1449,7 +1448,7 @@ async def get_prop_async( @final async def set_prop_async( - self, did: str, siid: int, piid: int, value: any, + self, did: str, siid: int, piid: int, value: Any, timeout_ms: int = 10000 ) -> dict: payload_obj: dict = { @@ -1580,13 +1579,13 @@ async def exec_action_group_list_async( @final @property - def on_dev_list_changed(self) -> Callable[[any, list[str]], asyncio.Future]: + def on_dev_list_changed(self) -> Callable[[Any, list[str]], asyncio.Future]: return self._on_dev_list_changed @final @on_dev_list_changed.setter def on_dev_list_changed( - self, func: Callable[[any, list[str]], asyncio.Future] + self, func: Callable[[Any, list[str]], asyncio.Future] ) -> None: """run in main loop.""" self._on_dev_list_changed = func @@ -1731,8 +1730,8 @@ def __mips_publish( def __request( self, topic: str, payload: str, - on_reply: Callable[[str, any], None], - on_reply_ctx: any = None, timeout_ms: int = 10000 + on_reply: Callable[[str, Any], None], + on_reply_ctx: Any = None, timeout_ms: int = 10000 ) -> bool: if topic is None or payload is None or on_reply is None: raise MIoTMipsError('invalid params') @@ -1745,8 +1744,8 @@ def __request( return self._mips_send_cmd(type_=MipsCmdType.CALL_API, data=req_data) def __reg_broadcast( - self, topic: str, handler: Callable[[str, str, any], None], - handler_ctx: any + self, topic: str, handler: Callable[[str, str, Any], None], + handler_ctx: Any ) -> bool: return self._mips_send_cmd( type_=MipsCmdType.REG_BROADCAST, @@ -1764,7 +1763,7 @@ async def __request_async( ) -> dict: fut_handler: asyncio.Future = self.main_loop.create_future() - def on_msg_reply(payload: str, ctx: any): + def on_msg_reply(payload: str, ctx: Any): fut: asyncio.Future = ctx if fut: self.main_loop.call_soon_threadsafe(fut.set_result, payload) diff --git a/custom_components/xiaomi_home/miot/miot_spec.py b/custom_components/xiaomi_home/miot/miot_spec.py index 3df70f1..bae7e79 100644 --- a/custom_components/xiaomi_home/miot/miot_spec.py +++ b/custom_components/xiaomi_home/miot/miot_spec.py @@ -49,7 +49,7 @@ import json import platform import time -from typing import Optional +from typing import Any, Optional from urllib.parse import urlencode from urllib.request import Request, urlopen import logging @@ -78,9 +78,9 @@ class MIoTSpecBase: # External params platform: str - device_class: any + device_class: Any icon: str - external_unit: any + external_unit: Any spec_id: str @@ -166,7 +166,7 @@ def readable(self) -> bool: def notifiable(self): return self._notifiable - def value_format(self, value: any) -> any: + def value_format(self, value: Any) -> Any: if value is None: return None if self.format_ == 'int': @@ -296,7 +296,7 @@ class MIoTSpecInstance: # External params platform: str - device_class: any + device_class: Any icon: str def __init__( diff --git a/custom_components/xiaomi_home/miot/miot_storage.py b/custom_components/xiaomi_home/miot/miot_storage.py index dfd6b28..85b25c9 100644 --- a/custom_components/xiaomi_home/miot/miot_storage.py +++ b/custom_components/xiaomi_home/miot/miot_storage.py @@ -56,7 +56,7 @@ from datetime import datetime, timezone from enum import Enum, auto from pathlib import Path -from typing import Optional, Union +from typing import Any, Optional, Union import logging from urllib.request import Request, urlopen from cryptography.hazmat.primitives import serialization @@ -419,7 +419,7 @@ async def clear_async(self) -> bool: return await fut def update_user_config( - self, uid: str, cloud_server: str, config: Optional[dict[str, any]], + self, uid: str, cloud_server: str, config: Optional[dict[str, Any]], replace: bool = False ) -> bool: if config is not None and len(config) == 0: @@ -443,7 +443,7 @@ def update_user_config( domain=config_domain, name=config_name, data=local_config) async def update_user_config_async( - self, uid: str, cloud_server: str, config: Optional[dict[str, any]], + self, uid: str, cloud_server: str, config: Optional[dict[str, Any]], replace: bool = False ) -> bool: """Update user configuration. @@ -480,7 +480,7 @@ async def update_user_config_async( def load_user_config( self, uid: str, cloud_server: str, keys: Optional[list[str]] = None - ) -> dict[str, any]: + ) -> dict[str, Any]: if keys is not None and len(keys) == 0: # Do nothing return {} @@ -494,7 +494,7 @@ def load_user_config( async def load_user_config_async( self, uid: str, cloud_server: str, keys: Optional[list[str]] = None - ) -> dict[str, any]: + ) -> dict[str, Any]: """Load user configuration. Args: @@ -503,7 +503,7 @@ async def load_user_config_async( query key list, return all config item if keys is None Returns: - dict[str, any]: query result + dict[str, Any]: query result """ if keys is not None and len(keys) == 0: # Do nothing diff --git a/custom_components/xiaomi_home/sensor.py b/custom_components/xiaomi_home/sensor.py index 7d2e074..b5ff00d 100644 --- a/custom_components/xiaomi_home/sensor.py +++ b/custom_components/xiaomi_home/sensor.py @@ -47,6 +47,7 @@ """ from __future__ import annotations import logging +from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -107,7 +108,7 @@ def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None: self._attr_icon = spec.icon @property - def native_value(self) -> any: + def native_value(self) -> Any: """Return the current value of the sensor.""" if self._value_range and isinstance(self._value, (int, float)): if ( diff --git a/custom_components/xiaomi_home/water_heater.py b/custom_components/xiaomi_home/water_heater.py index ee6a91d..aa7fe67 100644 --- a/custom_components/xiaomi_home/water_heater.py +++ b/custom_components/xiaomi_home/water_heater.py @@ -47,7 +47,7 @@ """ from __future__ import annotations import logging -from typing import Optional +from typing import Any, Optional from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -93,7 +93,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity): _prop_target_temp: Optional[MIoTSpecProperty] _prop_mode: Optional[MIoTSpecProperty] - _mode_list: Optional[dict[any, any]] + _mode_list: Optional[dict[Any, Any]] def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData @@ -164,7 +164,7 @@ async def async_turn_off(self) -> None: """Turn the water heater off.""" await self.set_property_async(prop=self._prop_on, value=False) - async def async_set_temperature(self, **kwargs: any) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set the temperature the water heater should heat water to.""" await self.set_property_async( prop=self._prop_target_temp, value=kwargs[ATTR_TEMPERATURE]) diff --git a/test/test_lan.py b/test/test_lan.py index 4f96725..a6051c0 100755 --- a/test/test_lan.py +++ b/test/test_lan.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Unit test for miot_lan.py.""" +from typing import Any import pytest import asyncio from zeroconf import IPVersion @@ -79,7 +80,7 @@ async def test_lan_async(test_devices: dict): evt_push_unavailable = asyncio.Event() await miot_lan.vote_for_lan_ctrl_async(key='test', vote=True) - async def device_state_change(did: str, state: dict, ctx: any): + async def device_state_change(did: str, state: dict, ctx: Any): print('device state change, ', did, state) if did != test_did: return