diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 481762c3..34f87343 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,7 +19,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest pytest-asyncio pytest-dependency zeroconf paho.mqtt psutil cryptography + pip install pytest pytest-asyncio pytest-dependency zeroconf paho.mqtt psutil cryptography slugify - name: Check rule format with pytest run: | diff --git a/custom_components/xiaomi_home/__init__.py b/custom_components/xiaomi_home/__init__.py index 694154d8..fb1b5c78 100644 --- a/custom_components/xiaomi_home/__init__.py +++ b/custom_components/xiaomi_home/__init__.py @@ -54,6 +54,7 @@ from homeassistant.components import persistent_notification from homeassistant.helpers import device_registry, entity_registry +from .miot.common import slugify_did from .miot.miot_storage import ( DeviceManufacturer, MIoTStorage, MIoTCert) from .miot.miot_spec import ( @@ -92,7 +93,7 @@ def ha_persistent_notify( """Send messages in Notifications dialog box.""" if title: persistent_notification.async_create( - hass=hass, message=message, + hass=hass, message=message or '', title=title, notification_id=notify_id) else: persistent_notification.async_dismiss( @@ -125,9 +126,8 @@ def ha_persistent_notify( miot_devices: list[MIoTDevice] = [] er = entity_registry.async_get(hass=hass) for did, info in miot_client.device_list.items(): - spec_instance: MIoTSpecInstance = await spec_parser.parse( - urn=info['urn']) - if spec_instance is None: + spec_instance = await spec_parser.parse(urn=info['urn']) + if not isinstance(spec_instance, MIoTSpecInstance): _LOGGER.error('spec content is None, %s, %s', did, info) continue device: MIoTDevice = MIoTDevice( @@ -155,7 +155,8 @@ def ha_persistent_notify( for entity in filter_entities: device.entity_list[platform].remove(entity) entity_id = device.gen_service_entity_id( - ha_domain=platform, siid=entity.spec.iid) + ha_domain=platform, + siid=entity.spec.iid) # type: ignore if er.async_get(entity_id_or_uuid=entity_id): er.async_remove(entity_id=entity_id) if platform in device.prop_list: @@ -208,12 +209,7 @@ def ha_persistent_notify( if er.async_get(entity_id_or_uuid=entity_id): er.async_remove(entity_id=entity_id) # Action debug - if miot_client.action_debug: - if 'notify' in device.action_list: - # Add text entity for debug action - device.action_list['action_text'] = ( - device.action_list['notify']) - else: + if not miot_client.action_debug: # Remove text entity for debug action for action in device.action_list.get('notify', []): entity_id = device.gen_action_entity_id( @@ -221,6 +217,21 @@ def ha_persistent_notify( siid=action.service.iid, aiid=action.iid) if er.async_get(entity_id_or_uuid=entity_id): er.async_remove(entity_id=entity_id) + # Binary sensor display + if not miot_client.display_binary_bool: + for prop in device.prop_list.get('binary_sensor', []): + entity_id = device.gen_prop_entity_id( + ha_domain='binary_sensor', spec_name=prop.name, + siid=prop.service.iid, piid=prop.iid) + if er.async_get(entity_id_or_uuid=entity_id): + er.async_remove(entity_id=entity_id) + if not miot_client.display_binary_text: + for prop in device.prop_list.get('binary_sensor', []): + entity_id = device.gen_prop_entity_id( + ha_domain='sensor', spec_name=prop.name, + siid=prop.service.iid, piid=prop.iid) + if er.async_get(entity_id_or_uuid=entity_id): + er.async_remove(entity_id=entity_id) hass.data[DOMAIN]['devices'][config_entry.entry_id] = miot_devices await hass.config_entries.async_forward_entry_setups( @@ -237,7 +248,7 @@ def ha_persistent_notify( device_entry = dr.async_get_device( identifiers={( DOMAIN, - MIoTDevice.gen_did_tag( + slugify_did( cloud_server=config_entry.data['cloud_server'], did=did))}, connections=None) @@ -330,21 +341,10 @@ async def async_remove_config_entry_device( 'remove device failed, invalid domain, %s, %s', device_entry.id, device_entry.identifiers) return False - device_info = identifiers[1].split('_') - if len(device_info) != 2: - _LOGGER.error( - 'remove device failed, invalid device info, %s, %s', - device_entry.id, device_entry.identifiers) - return False - did = device_info[1] - if did not in miot_client.device_list: - _LOGGER.error( - 'remove device failed, device not found, %s, %s', - device_entry.id, device_entry.identifiers) - return False + # Remove device - await miot_client.remove_device_async(did) + await miot_client.remove_device2_async(did_tag=identifiers[1]) device_registry.async_get(hass).async_remove_device(device_entry.id) _LOGGER.info( - 'remove device, %s, %s, %s', device_info[0], did, device_entry.id) + 'remove device, %s, %s', identifiers[1], device_entry.id) return True diff --git a/custom_components/xiaomi_home/binary_sensor.py b/custom_components/xiaomi_home/binary_sensor.py index 9ec6e833..aca45d88 100644 --- a/custom_components/xiaomi_home/binary_sensor.py +++ b/custom_components/xiaomi_home/binary_sensor.py @@ -68,9 +68,10 @@ async def async_setup_entry( new_entities = [] for miot_device in device_list: - for prop in miot_device.prop_list.get('binary_sensor', []): - new_entities.append(BinarySensor( - miot_device=miot_device, spec=prop)) + if miot_device.miot_client.display_binary_bool: + for prop in miot_device.prop_list.get('binary_sensor', []): + new_entities.append(BinarySensor( + miot_device=miot_device, spec=prop)) if new_entities: async_add_entities(new_entities) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index bd4cfe36..fb3dc457 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -156,64 +156,56 @@ def __init__( _LOGGER.error( 'unknown on property, %s', self.entity_id) elif prop.name == 'mode': - if ( - not isinstance(prop.value_list, list) - or not prop.value_list - ): + if not prop.value_list: _LOGGER.error( 'invalid mode value_list, %s', self.entity_id) continue self._hvac_mode_map = {} - for item in prop.value_list: - if item['name'].lower() in {'off', 'idle'}: - self._hvac_mode_map[item['value']] = HVACMode.OFF - elif item['name'].lower() in {'auto'}: - self._hvac_mode_map[item['value']] = HVACMode.AUTO - elif item['name'].lower() in {'cool'}: - self._hvac_mode_map[item['value']] = HVACMode.COOL - elif item['name'].lower() in {'heat'}: - self._hvac_mode_map[item['value']] = HVACMode.HEAT - elif item['name'].lower() in {'dry'}: - self._hvac_mode_map[item['value']] = HVACMode.DRY - elif item['name'].lower() in {'fan'}: - self._hvac_mode_map[item['value']] = HVACMode.FAN_ONLY + for item in prop.value_list.items: + if item.name in {'off', 'idle'}: + self._hvac_mode_map[item.value] = HVACMode.OFF + elif item.name in {'auto'}: + self._hvac_mode_map[item.value] = HVACMode.AUTO + elif item.name in {'cool'}: + self._hvac_mode_map[item.value] = HVACMode.COOL + elif item.name in {'heat'}: + self._hvac_mode_map[item.value] = HVACMode.HEAT + elif item.name in {'dry'}: + self._hvac_mode_map[item.value] = HVACMode.DRY + elif item.name in {'fan'}: + self._hvac_mode_map[item.value] = HVACMode.FAN_ONLY self._attr_hvac_modes = list(self._hvac_mode_map.values()) self._prop_mode = prop elif prop.name == 'target-temperature': - if not isinstance(prop.value_range, dict): + if not prop.value_range: _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_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 == 'target-humidity': - if not isinstance(prop.value_range, dict): + if not prop.value_range: _LOGGER.error( 'invalid target-humidity value_range format, %s', self.entity_id) continue - self._attr_min_humidity = prop.value_range['min'] - self._attr_max_humidity = prop.value_range['max'] + self._attr_min_humidity = prop.value_range.min_ + self._attr_max_humidity = prop.value_range.max_ self._attr_supported_features |= ( ClimateEntityFeature.TARGET_HUMIDITY) self._prop_target_humi = prop elif prop.name == 'fan-level': - if ( - not isinstance(prop.value_list, list) - or not prop.value_list - ): + if not prop.value_list: _LOGGER.error( 'invalid fan-level value_list, %s', self.entity_id) continue - self._fan_mode_map = { - item['value']: item['description'] - for item in prop.value_list} + self._fan_mode_map = prop.value_list.to_map() self._attr_fan_modes = list(self._fan_mode_map.values()) self._attr_supported_features |= ClimateEntityFeature.FAN_MODE self._prop_fan_level = prop @@ -269,8 +261,8 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: 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( - map_=self._hvac_mode_map, description=hvac_mode) + mode_value = self.get_map_key( + map_=self._hvac_mode_map, value=hvac_mode) if ( mode_value is None or not await self.set_property_async( @@ -339,8 +331,8 @@ async def async_set_swing_mode(self, swing_mode): async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" - mode_value = self.get_map_value( - map_=self._fan_mode_map, description=fan_mode) + mode_value = self.get_map_key( + map_=self._fan_mode_map, value=fan_mode) if mode_value is None or not await self.set_property_async( prop=self._prop_fan_level, value=mode_value): raise RuntimeError( @@ -376,9 +368,9 @@ 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: return HVACMode.OFF - return self.get_map_description( + return self.get_map_key( map_=self._hvac_mode_map, - key=self.get_prop_value(prop=self._prop_mode)) + value=self.get_prop_value(prop=self._prop_mode)) @property def fan_mode(self) -> Optional[str]: @@ -386,7 +378,7 @@ def fan_mode(self) -> Optional[str]: Requires ClimateEntityFeature.FAN_MODE. """ - return self.get_map_description( + return self.get_map_value( map_=self._fan_mode_map, key=self.get_prop_value(prop=self._prop_fan_level)) @@ -446,8 +438,8 @@ def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None: }.get(v_ac_state['M'], None) if mode: self.set_prop_value( - prop=self._prop_mode, value=self.get_map_value( - map_=self._hvac_mode_map, description=mode)) + prop=self._prop_mode, value=self.get_map_key( + map_=self._hvac_mode_map, value=mode)) # T: target temperature if 'T' in v_ac_state and self._prop_target_temp: self.set_prop_value(prop=self._prop_target_temp, @@ -517,29 +509,24 @@ def __init__( ClimateEntityFeature.TURN_OFF) self._prop_on = prop elif prop.name == 'target-temperature': - if not isinstance(prop.value_range, dict): + if not prop.value_range: _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_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 == 'heat-level': - if ( - not isinstance(prop.value_list, list) - or not prop.value_list - ): + if 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._heat_level_map = prop.value_list.to_map() self._attr_preset_modes = list(self._heat_level_map.values()) self._attr_supported_features |= ( ClimateEntityFeature.PRESET_MODE) @@ -582,8 +569,8 @@ 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)) + value=self.get_map_key( + map_=self._heat_level_map, value=preset_mode)) @property def target_temperature(self) -> Optional[float]: @@ -613,7 +600,7 @@ def hvac_mode(self) -> Optional[HVACMode]: @property def preset_mode(self) -> Optional[str]: return ( - self.get_map_description( + self.get_map_value( 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/config_flow.py b/custom_components/xiaomi_home/config_flow.py index 5b78c27b..7c0d20a9 100644 --- a/custom_components/xiaomi_home/config_flow.py +++ b/custom_components/xiaomi_home/config_flow.py @@ -124,6 +124,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _area_name_rule: str _action_debug: bool _hide_non_standard_entities: bool + _display_binary_mode: list[str] _display_devices_changed_notify: list[str] _cloud_server: str @@ -158,6 +159,7 @@ def __init__(self) -> None: self._area_name_rule = self.DEFAULT_AREA_NAME_RULE self._action_debug = False self._hide_non_standard_entities = False + self._display_binary_mode = ['bool'] self._display_devices_changed_notify = ['add', 'del', 'offline'] self._auth_info = {} self._nick_name = DEFAULT_NICK_NAME @@ -473,6 +475,7 @@ async def async_step_oauth( await self._miot_oauth.deinit_async() self._miot_oauth = None return self.async_show_progress_done(next_step_id='homes_select') + # pylint: disable=unexpected-keyword-arg return self.async_show_progress( step_id='oauth', progress_action='oauth', @@ -481,7 +484,7 @@ async def async_step_oauth( f'', 'link_right': '' }, - progress_task=self._cc_task_oauth, + progress_task=self._cc_task_oauth, # type: ignore ) async def __check_oauth_async(self) -> None: @@ -727,6 +730,8 @@ async def async_step_advanced_options( 'action_debug', self._action_debug) self._hide_non_standard_entities = user_input.get( 'hide_non_standard_entities', self._hide_non_standard_entities) + self._display_binary_mode = user_input.get( + 'display_binary_mode', self._display_binary_mode) self._display_devices_changed_notify = user_input.get( 'display_devices_changed_notify', self._display_devices_changed_notify) @@ -749,6 +754,12 @@ async def async_step_advanced_options( 'hide_non_standard_entities', default=self._hide_non_standard_entities # type: ignore ): bool, + vol.Required( + 'display_binary_mode', + default=self._display_binary_mode # type: ignore + ): cv.multi_select( + self._miot_i18n.translate( + key='config.binary_mode')), # type: ignore vol.Required( 'display_devices_changed_notify', default=self._display_devices_changed_notify # type: ignore @@ -931,6 +942,7 @@ async def config_flow_done(self): 'action_debug': self._action_debug, 'hide_non_standard_entities': self._hide_non_standard_entities, + 'display_binary_mode': self._display_binary_mode, 'display_devices_changed_notify': self._display_devices_changed_notify }) @@ -972,6 +984,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): _devices_filter: dict _action_debug: bool _hide_non_standard_entities: bool + _display_binary_mode: list[str] _display_devs_notify: list[str] _oauth_redirect_url_full: str @@ -986,6 +999,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): _nick_name_new: Optional[str] _action_debug_new: bool _hide_non_standard_entities_new: bool + _display_binary_mode_new: list[str] _update_user_info: bool _update_devices: bool _update_trans_rules: bool @@ -1024,6 +1038,8 @@ def __init__(self, config_entry: config_entries.ConfigEntry): self._action_debug = self._entry_data.get('action_debug', False) self._hide_non_standard_entities = self._entry_data.get( 'hide_non_standard_entities', False) + self._display_binary_mode = self._entry_data.get( + 'display_binary_mode', ['text']) self._display_devs_notify = self._entry_data.get( 'display_devices_changed_notify', ['add', 'del', 'offline']) self._home_selected_list = list( @@ -1042,6 +1058,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry): self._nick_name_new = None self._action_debug_new = False self._hide_non_standard_entities_new = False + self._display_binary_mode_new = [] self._update_user_info = False self._update_devices = False self._update_trans_rules = False @@ -1196,7 +1213,7 @@ async def async_step_oauth(self, user_input=None): err, traceback.format_exc()) self._cc_config_rc = str(err) return self.async_show_progress_done(next_step_id='oauth_error') - + # pylint: disable=unexpected-keyword-arg return self.async_show_progress( step_id='oauth', progress_action='oauth', @@ -1205,7 +1222,7 @@ async def async_step_oauth(self, user_input=None): f'', 'link_right': '' }, - progress_task=self._cc_task_oauth, + progress_task=self._cc_task_oauth, # type: ignore ) async def __check_oauth_async(self) -> None: @@ -1308,6 +1325,12 @@ async def async_step_config_options(self, user_input=None): 'hide_non_standard_entities', default=self._hide_non_standard_entities # type: ignore ): bool, + vol.Required( + 'display_binary_mode', + default=self._display_binary_mode # type: ignore + ): cv.multi_select( + self._miot_i18n.translate( + 'config.binary_mode')), # type: ignore vol.Required( 'update_trans_rules', default=self._update_trans_rules # type: ignore @@ -1336,6 +1359,8 @@ async def async_step_config_options(self, user_input=None): 'action_debug', self._action_debug) self._hide_non_standard_entities_new = user_input.get( 'hide_non_standard_entities', self._hide_non_standard_entities) + self._display_binary_mode_new = user_input.get( + 'display_binary_mode', self._display_binary_mode) self._display_devs_notify = user_input.get( 'display_devices_changed_notify', self._display_devs_notify) self._update_trans_rules = user_input.get( @@ -1939,6 +1964,10 @@ async def async_step_config_confirm(self, user_input=None): self._entry_data['hide_non_standard_entities'] = ( self._hide_non_standard_entities_new) self._need_reload = True + if set(self._display_binary_mode) != set(self._display_binary_mode_new): + self._entry_data['display_binary_mode'] = ( + self._display_binary_mode_new) + self._need_reload = True # Update display_devices_changed_notify self._entry_data['display_devices_changed_notify'] = ( self._display_devs_notify) diff --git a/custom_components/xiaomi_home/cover.py b/custom_components/xiaomi_home/cover.py index d8236c75..78a6a028 100644 --- a/custom_components/xiaomi_home/cover.py +++ b/custom_components/xiaomi_home/cover.py @@ -132,53 +132,47 @@ def __init__( # properties for prop in entity_data.props: if prop.name == 'motor-control': - if ( - not isinstance(prop.value_list, list) - or not prop.value_list - ): + if not prop.value_list: _LOGGER.error( 'motor-control value_list is None, %s', self.entity_id) continue - for item in prop.value_list: - if item['name'].lower() in ['open']: + for item in prop.value_list.items: + if item.name in {'open'}: self._attr_supported_features |= ( CoverEntityFeature.OPEN) - self._prop_motor_value_open = item['value'] - elif item['name'].lower() in ['close']: + self._prop_motor_value_open = item.value + elif item.name in {'close'}: self._attr_supported_features |= ( CoverEntityFeature.CLOSE) - self._prop_motor_value_close = item['value'] - elif item['name'].lower() in ['pause']: + self._prop_motor_value_close = item.value + elif item.name in {'pause'}: self._attr_supported_features |= ( CoverEntityFeature.STOP) - self._prop_motor_value_pause = item['value'] + self._prop_motor_value_pause = item.value self._prop_motor_control = prop elif prop.name == 'status': - if ( - not isinstance(prop.value_list, list) - or not prop.value_list - ): + if not prop.value_list: _LOGGER.error( 'status value_list is None, %s', self.entity_id) continue - for item in prop.value_list: - if item['name'].lower() in ['opening', 'open']: - self._prop_status_opening = item['value'] - elif item['name'].lower() in ['closing', 'close']: - self._prop_status_closing = item['value'] - elif item['name'].lower() in ['stop', 'pause']: - self._prop_status_stop = item['value'] + for item in prop.value_list.items: + if item.name in {'opening', 'open'}: + self._prop_status_opening = item.value + elif item.name in {'closing', 'close'}: + self._prop_status_closing = item.value + elif item.name in {'stop', 'pause'}: + self._prop_status_stop = item.value self._prop_status = prop elif prop.name == 'current-position': self._prop_current_position = prop elif prop.name == 'target-position': - if not isinstance(prop.value_range, dict): + if not prop.value_range: _LOGGER.error( 'invalid target-position value_range format, %s', self.entity_id) continue - self._prop_position_value_min = prop.value_range['min'] - self._prop_position_value_max = prop.value_range['max'] + self._prop_position_value_min = prop.value_range.min_ + self._prop_position_value_max = prop.value_range.max_ self._prop_position_value_range = ( self._prop_position_value_max - self._prop_position_value_min) diff --git a/custom_components/xiaomi_home/event.py b/custom_components/xiaomi_home/event.py index 78922905..85fbf338 100644 --- a/custom_components/xiaomi_home/event.py +++ b/custom_components/xiaomi_home/event.py @@ -85,6 +85,8 @@ 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: dict[str, Any] | None = None + ) -> None: """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 90220db0..a28b989c 100644 --- a/custom_components/xiaomi_home/fan.py +++ b/custom_components/xiaomi_home/fan.py @@ -87,7 +87,7 @@ async def async_setup_entry( class Fan(MIoTServiceEntity, FanEntity): """Fan entities for Xiaomi Home.""" # pylint: disable=unused-argument - _prop_on: Optional[MIoTSpecProperty] + _prop_on: MIoTSpecProperty _prop_fan_level: Optional[MIoTSpecProperty] _prop_mode: Optional[MIoTSpecProperty] _prop_horizontal_swing: Optional[MIoTSpecProperty] @@ -100,7 +100,7 @@ class Fan(MIoTServiceEntity, FanEntity): _speed_step: int _speed_names: Optional[list] _speed_name_map: Optional[dict[int, str]] - _mode_list: Optional[dict[Any, Any]] + _mode_map: Optional[dict[Any, Any]] def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData @@ -111,7 +111,7 @@ def __init__( self._attr_current_direction = None self._attr_supported_features = FanEntityFeature(0) - self._prop_on = None + # _prop_on is required self._prop_fan_level = None self._prop_mode = None self._prop_horizontal_swing = None @@ -124,7 +124,7 @@ def __init__( self._speed_names = [] self._speed_name_map = {} - self._mode_list = None + self._mode_map = None # properties for prop in entity_data.props: @@ -133,42 +133,34 @@ def __init__( self._attr_supported_features |= FanEntityFeature.TURN_OFF self._prop_on = prop elif prop.name == 'fan-level': - if isinstance(prop.value_range, dict): + if prop.value_range: # Fan level with value-range - self._speed_min = prop.value_range['min'] - self._speed_max = prop.value_range['max'] - self._speed_step = prop.value_range['step'] + self._speed_min = prop.value_range.min_ + self._speed_max = prop.value_range.max_ + self._speed_step = prop.value_range.step self._attr_speed_count = int(( self._speed_max - self._speed_min)/self._speed_step)+1 self._attr_supported_features |= FanEntityFeature.SET_SPEED self._prop_fan_level = prop elif ( self._prop_fan_level is None - and isinstance(prop.value_list, list) and prop.value_list ): # Fan level with value-list # Fan level with value-range is prior to fan level with # value-list when a fan has both fan level properties. - self._speed_name_map = { - item['value']: item['description'] - for item in prop.value_list} + self._speed_name_map = prop.value_list.to_map() self._speed_names = list(self._speed_name_map.values()) - self._attr_speed_count = len(prop.value_list) + self._attr_speed_count = len(self._speed_names) self._attr_supported_features |= FanEntityFeature.SET_SPEED self._prop_fan_level = prop elif prop.name == 'mode': - if ( - not isinstance(prop.value_list, list) - or not prop.value_list - ): + if not prop.value_list: _LOGGER.error( 'mode value_list is None, %s', self.entity_id) continue - self._mode_list = { - item['value']: item['description'] - for item in prop.value_list} - self._attr_preset_modes = list(self._mode_list.values()) + self._mode_map = prop.value_list.to_map() + self._attr_preset_modes = list(self._mode_map.values()) self._attr_supported_features |= FanEntityFeature.PRESET_MODE self._prop_mode = prop elif prop.name == 'horizontal-swing': @@ -178,16 +170,11 @@ def __init__( if prop.format_ == 'bool': self._prop_wind_reverse_forward = False self._prop_wind_reverse_reverse = True - elif ( - isinstance(prop.value_list, list) - and prop.value_list - ): - for item in prop.value_list: - if item['name'].lower() in {'foreward'}: - self._prop_wind_reverse_forward = item['value'] - elif item['name'].lower() in { - 'reversal', 'reverse'}: - self._prop_wind_reverse_reverse = item['value'] + elif prop.value_list: + for item in prop.value_list.items: + if item.name in {'foreward'}: + self._prop_wind_reverse_forward = item.value + self._prop_wind_reverse_reverse = item.value if ( self._prop_wind_reverse_forward is None or self._prop_wind_reverse_reverse is None @@ -199,21 +186,9 @@ def __init__( self._attr_supported_features |= FanEntityFeature.DIRECTION self._prop_wind_reverse = prop - def __get_mode_description(self, key: int) -> Optional[str]: - if self._mode_list is None: - return None - return self._mode_list.get(key, None) - - def __get_mode_value(self, description: str) -> Optional[int]: - if self._mode_list is None: - return None - for key, value in self._mode_list.items(): - if value == description: - return key - return None - async def async_turn_on( - self, percentage: int = None, preset_mode: str = None, **kwargs: Any + self, percentage: Optional[int] = None, + preset_mode: Optional[str] = None, **kwargs: Any ) -> None: """Turn the fan on. @@ -225,12 +200,12 @@ async def async_turn_on( # percentage if percentage: if self._speed_names: - speed = percentage_to_ordered_list_item( - self._speed_names, percentage) - speed_value = self.get_map_value( - map_=self._speed_name_map, description=speed) await self.set_property_async( - prop=self._prop_fan_level, value=speed_value) + prop=self._prop_fan_level, + value=self.get_map_value( + map_=self._speed_name_map, + key=percentage_to_ordered_list_item( + self._speed_names, percentage))) else: await self.set_property_async( prop=self._prop_fan_level, @@ -241,7 +216,8 @@ async def async_turn_on( if preset_mode: await self.set_property_async( self._prop_mode, - value=self.__get_mode_value(description=preset_mode)) + value=self.get_map_key( + map_=self._mode_map, value=preset_mode)) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fan off.""" @@ -255,12 +231,12 @@ async def async_set_percentage(self, percentage: int) -> None: """Set the percentage of the fan speed.""" if percentage > 0: if self._speed_names: - speed = percentage_to_ordered_list_item( - self._speed_names, percentage) - speed_value = self.get_map_value( - map_=self._speed_name_map, description=speed) await self.set_property_async( - prop=self._prop_fan_level, value=speed_value) + prop=self._prop_fan_level, + value=self.get_map_value( + map_=self._speed_name_map, + key=percentage_to_ordered_list_item( + self._speed_names, percentage))) else: await self.set_property_async( prop=self._prop_fan_level, @@ -277,7 +253,8 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode.""" await self.set_property_async( self._prop_mode, - value=self.__get_mode_value(description=preset_mode)) + value=self.get_map_key( + map_=self._mode_map, value=preset_mode)) async def async_set_direction(self, direction: str) -> None: """Set the direction of the fan.""" @@ -306,7 +283,8 @@ def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., auto, smart, eco, favorite.""" return ( - self.__get_mode_description( + self.get_map_value( + map_=self._mode_map, key=self.get_prop_value(prop=self._prop_mode)) if self._prop_mode else None) diff --git a/custom_components/xiaomi_home/humidifier.py b/custom_components/xiaomi_home/humidifier.py index 9739da43..1bcd5c80 100644 --- a/custom_components/xiaomi_home/humidifier.py +++ b/custom_components/xiaomi_home/humidifier.py @@ -97,7 +97,7 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity): _prop_target_humidity: Optional[MIoTSpecProperty] _prop_humidity: Optional[MIoTSpecProperty] - _mode_list: dict[Any, Any] + _mode_map: dict[Any, Any] def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData @@ -110,7 +110,7 @@ def __init__( self._prop_mode = None self._prop_target_humidity = None self._prop_humidity = None - self._mode_list = None + self._mode_map = None # properties for prop in entity_data.props: @@ -119,28 +119,23 @@ def __init__( self._prop_on = prop # target-humidity elif prop.name == 'target-humidity': - if not isinstance(prop.value_range, dict): + if not prop.value_range: _LOGGER.error( 'invalid target-humidity value_range format, %s', self.entity_id) continue - self._attr_min_humidity = prop.value_range['min'] - self._attr_max_humidity = prop.value_range['max'] + self._attr_min_humidity = prop.value_range.min_ + self._attr_max_humidity = prop.value_range.max_ self._prop_target_humidity = prop # mode elif prop.name == 'mode': - if ( - not isinstance(prop.value_list, list) - or not prop.value_list - ): + if not prop.value_list: _LOGGER.error( 'mode value_list is None, %s', self.entity_id) continue - self._mode_list = { - item['value']: item['description'] - for item in prop.value_list} + self._mode_map = prop.value_list.to_map() self._attr_available_modes = list( - self._mode_list.values()) + self._mode_map.values()) self._attr_supported_features |= HumidifierEntityFeature.MODES self._prop_mode = prop # relative-humidity @@ -163,7 +158,8 @@ async def async_set_humidity(self, humidity: int) -> None: async def async_set_mode(self, mode: str) -> None: """Set new target preset mode.""" await self.set_property_async( - prop=self._prop_mode, value=self.__get_mode_value(description=mode)) + prop=self._prop_mode, + value=self.get_map_key(map_=self._mode_map, value=mode)) @property def is_on(self) -> Optional[bool]: @@ -183,20 +179,6 @@ def target_humidity(self) -> Optional[int]: @property def mode(self) -> Optional[str]: """Return the current preset mode.""" - return self.__get_mode_description( + return self.get_map_value( + map_=self._mode_map, key=self.get_prop_value(prop=self._prop_mode)) - - def __get_mode_description(self, key: int) -> Optional[str]: - """Convert mode value to description.""" - if self._mode_list is None: - return None - return self._mode_list.get(key, None) - - def __get_mode_value(self, description: str) -> Optional[int]: - """Convert mode description to value.""" - if self._mode_list is None: - return None - for key, value in self._mode_list.items(): - if value == description: - return key - return None diff --git a/custom_components/xiaomi_home/light.py b/custom_components/xiaomi_home/light.py index 666464ef..16676623 100644 --- a/custom_components/xiaomi_home/light.py +++ b/custom_components/xiaomi_home/light.py @@ -96,14 +96,14 @@ class Light(MIoTServiceEntity, LightEntity): """Light entities for Xiaomi Home.""" # pylint: disable=unused-argument _VALUE_RANGE_MODE_COUNT_MAX = 30 - _prop_on: Optional[MIoTSpecProperty] + _prop_on: MIoTSpecProperty _prop_brightness: Optional[MIoTSpecProperty] _prop_color_temp: Optional[MIoTSpecProperty] _prop_color: Optional[MIoTSpecProperty] _prop_mode: Optional[MIoTSpecProperty] _brightness_scale: Optional[tuple[int, int]] - _mode_list: Optional[dict[Any, Any]] + _mode_map: Optional[dict[Any, Any]] def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData @@ -122,7 +122,7 @@ def __init__( self._prop_color = None self._prop_mode = None self._brightness_scale = None - self._mode_list = None + self._mode_map = None # properties for prop in entity_data.props: @@ -131,20 +131,17 @@ def __init__( self._prop_on = prop # brightness if prop.name == 'brightness': - if isinstance(prop.value_range, dict): + if prop.value_range: self._brightness_scale = ( - prop.value_range['min'], prop.value_range['max']) + prop.value_range.min_, prop.value_range.max_) self._prop_brightness = prop elif ( - self._mode_list is None - and isinstance(prop.value_list, list) + self._mode_map is None and prop.value_list ): # For value-list brightness - self._mode_list = { - item['value']: item['description'] - for item in prop.value_list} - self._attr_effect_list = list(self._mode_list.values()) + self._mode_map = prop.value_list.to_map() + self._attr_effect_list = list(self._mode_map.values()) self._attr_supported_features |= LightEntityFeature.EFFECT self._prop_mode = prop else: @@ -153,13 +150,13 @@ def __init__( continue # color-temperature if prop.name == 'color-temperature': - if not isinstance(prop.value_range, dict): + if not prop.value_range: _LOGGER.info( 'invalid color-temperature value_range format, %s', self.entity_id) continue - self._attr_min_color_temp_kelvin = prop.value_range['min'] - self._attr_max_color_temp_kelvin = prop.value_range['max'] + self._attr_min_color_temp_kelvin = prop.value_range.min_ + self._attr_max_color_temp_kelvin = prop.value_range.max_ self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) self._attr_color_mode = ColorMode.COLOR_TEMP self._prop_color_temp = prop @@ -171,20 +168,15 @@ def __init__( # mode if prop.name == 'mode': mode_list = None - if ( - isinstance(prop.value_list, list) - and prop.value_list - ): - mode_list = { - item['value']: item['description'] - for item in prop.value_list} - elif isinstance(prop.value_range, dict): + if prop.value_list: + mode_list = prop.value_list.to_map() + elif prop.value_range: mode_list = {} if ( int(( - prop.value_range['max'] - - prop.value_range['min'] - ) / prop.value_range['step']) + prop.value_range.max_ + - prop.value_range.min_ + ) / prop.value_range.step) > self._VALUE_RANGE_MODE_COUNT_MAX ): _LOGGER.info( @@ -192,13 +184,13 @@ def __init__( self.entity_id, prop.name, prop.value_range) else: for value in range( - prop.value_range['min'], - prop.value_range['max'], - prop.value_range['step']): + prop.value_range.min_, + prop.value_range.max_, + prop.value_range.step): mode_list[value] = f'mode {value}' if mode_list: - self._mode_list = mode_list - self._attr_effect_list = list(self._mode_list.values()) + self._mode_map = mode_list + self._attr_effect_list = list(self._mode_map.values()) self._attr_supported_features |= LightEntityFeature.EFFECT self._prop_mode = prop else: @@ -213,21 +205,6 @@ def __init__( self._attr_supported_color_modes.add(ColorMode.ONOFF) self._attr_color_mode = ColorMode.ONOFF - def __get_mode_description(self, key: int) -> Optional[str]: - """Convert mode value to description.""" - if self._mode_list is None: - return None - return self._mode_list.get(key, None) - - def __get_mode_value(self, description: str) -> Optional[int]: - """Convert mode description to value.""" - if self._mode_list is None: - return None - for key, value in self._mode_list.items(): - if value == description: - return key - return None - @property def is_on(self) -> Optional[bool]: """Return if the light is on.""" @@ -264,7 +241,8 @@ def rgb_color(self) -> Optional[tuple[int, int, int]]: @property def effect(self) -> Optional[str]: """Return the current mode.""" - return self.__get_mode_description( + return self.get_map_value( + map_=self._mode_map, key=self.get_prop_value(prop=self._prop_mode)) async def async_turn_on(self, **kwargs) -> None: @@ -275,7 +253,7 @@ async def async_turn_on(self, **kwargs) -> None: result: bool = False # on # Dirty logic for lumi.gateway.mgl03 indicator light - value_on = True if self._prop_on.format_ == 'bool' else 1 + value_on = True if self._prop_on.format_ == bool else 1 result = await self.set_property_async( prop=self._prop_on, value=value_on) # brightness @@ -303,11 +281,12 @@ async def async_turn_on(self, **kwargs) -> None: if ATTR_EFFECT in kwargs: result = await self.set_property_async( prop=self._prop_mode, - value=self.__get_mode_value(description=kwargs[ATTR_EFFECT])) + value=self.get_map_key( + map_=self._mode_map, value=kwargs[ATTR_EFFECT])) return result async def async_turn_off(self, **kwargs) -> None: """Turn the light off.""" # Dirty logic for lumi.gateway.mgl03 indicator light - value_on = False if self._prop_on.format_ == 'bool' else 0 + value_on = False if self._prop_on.format_ == bool else 0 return await self.set_property_async(prop=self._prop_on, value=value_on) diff --git a/custom_components/xiaomi_home/miot/common.py b/custom_components/xiaomi_home/miot/common.py index 0ee4f1dc..21c54397 100644 --- a/custom_components/xiaomi_home/miot/common.py +++ b/custom_components/xiaomi_home/miot/common.py @@ -45,13 +45,17 @@ Common utilities. """ +import asyncio import json from os import path import random from typing import Any, Optional import hashlib +from urllib.parse import urlencode +from urllib.request import Request, urlopen from paho.mqtt.matcher import MQTTMatcher import yaml +from slugify import slugify MIOT_ROOT_PATH: str = path.dirname(path.abspath(__file__)) @@ -83,10 +87,22 @@ def randomize_int(value: int, ratio: float) -> int: """Randomize an integer value.""" return int(value * (1 - ratio + random.random()*2*ratio)) + def randomize_float(value: float, ratio: float) -> float: """Randomize a float value.""" return value * (1 - ratio + random.random()*2*ratio) + +def slugify_name(name: str, separator: str = '_') -> str: + """Slugify a name.""" + return slugify(name, separator=separator) + + +def slugify_did(cloud_server: str, did: str) -> str: + """Slugify a device id.""" + return slugify(f'{cloud_server}_{did}', separator='_') + + class MIoTMatcher(MQTTMatcher): """MIoT Pub/Sub topic matcher.""" @@ -105,3 +121,68 @@ def get(self, topic: str) -> Optional[Any]: return self[topic] except KeyError: return None + + +class MIoTHttp: + """MIoT Common HTTP API.""" + @staticmethod + def get( + url: str, params: Optional[dict] = None, headers: Optional[dict] = None + ) -> Optional[str]: + full_url = url + if params: + encoded_params = urlencode(params) + full_url = f'{url}?{encoded_params}' + request = Request(full_url, method='GET', headers=headers or {}) + content: Optional[bytes] = None + with urlopen(request) as response: + content = response.read() + return str(content, 'utf-8') if content else None + + @staticmethod + def get_json( + url: str, params: Optional[dict] = None, headers: Optional[dict] = None + ) -> Optional[dict]: + response = MIoTHttp.get(url, params, headers) + return json.loads(response) if response else None + + @staticmethod + def post( + url: str, data: Optional[dict] = None, headers: Optional[dict] = None + ) -> Optional[str]: + pass + + @staticmethod + def post_json( + url: str, data: Optional[dict] = None, headers: Optional[dict] = None + ) -> Optional[dict]: + response = MIoTHttp.post(url, data, headers) + return json.loads(response) if response else None + + @staticmethod + async def get_async( + url: str, params: Optional[dict] = None, headers: Optional[dict] = None, + loop: Optional[asyncio.AbstractEventLoop] = None + ) -> Optional[str]: + # TODO: Use aiohttp + ev_loop = loop or asyncio.get_running_loop() + return await ev_loop.run_in_executor( + None, MIoTHttp.get, url, params, headers) + + @staticmethod + async def get_json_async( + url: str, params: Optional[dict] = None, headers: Optional[dict] = None, + loop: Optional[asyncio.AbstractEventLoop] = None + ) -> Optional[dict]: + ev_loop = loop or asyncio.get_running_loop() + return await ev_loop.run_in_executor( + None, MIoTHttp.get_json, url, params, headers) + + @ staticmethod + async def post_async( + url: str, data: Optional[dict] = None, headers: Optional[dict] = None, + loop: Optional[asyncio.AbstractEventLoop] = None + ) -> Optional[str]: + ev_loop = loop or asyncio.get_running_loop() + return await ev_loop.run_in_executor( + None, MIoTHttp.post, url, data, headers) diff --git a/custom_components/xiaomi_home/miot/i18n/de.json b/custom_components/xiaomi_home/miot/i18n/de.json index 9dce0e94..05ae2bf5 100644 --- a/custom_components/xiaomi_home/miot/i18n/de.json +++ b/custom_components/xiaomi_home/miot/i18n/de.json @@ -54,6 +54,10 @@ "enable": "aktivieren", "disable": "deaktivieren" }, + "binary_mode": { + "text": "Textsensor-Entität", + "bool": "Binärsensor-Entität" + }, "device_state": { "add": "hinzufügen", "del": "nicht verfügbar", diff --git a/custom_components/xiaomi_home/miot/i18n/en.json b/custom_components/xiaomi_home/miot/i18n/en.json index 7cf0ecbf..47187ad4 100644 --- a/custom_components/xiaomi_home/miot/i18n/en.json +++ b/custom_components/xiaomi_home/miot/i18n/en.json @@ -54,6 +54,10 @@ "enable": "Enable", "disable": "Disable" }, + "binary_mode": { + "text": "Text Sensor Entity", + "bool": "Binary Sensor Entity" + }, "device_state": { "add": "Add", "del": "Unavailable", diff --git a/custom_components/xiaomi_home/miot/i18n/es.json b/custom_components/xiaomi_home/miot/i18n/es.json index a71312f2..c6f78dfd 100644 --- a/custom_components/xiaomi_home/miot/i18n/es.json +++ b/custom_components/xiaomi_home/miot/i18n/es.json @@ -54,6 +54,10 @@ "enable": "habilitar", "disable": "deshabilitar" }, + "binary_mode": { + "text": "Entidad del sensor de texto", + "bool": "Entidad del sensor binario" + }, "device_state": { "add": "agregar", "del": "no disponible", diff --git a/custom_components/xiaomi_home/miot/i18n/fr.json b/custom_components/xiaomi_home/miot/i18n/fr.json index e64b614b..2789cc60 100644 --- a/custom_components/xiaomi_home/miot/i18n/fr.json +++ b/custom_components/xiaomi_home/miot/i18n/fr.json @@ -54,6 +54,10 @@ "enable": "activer", "disable": "désactiver" }, + "binary_mode": { + "text": "Entité du capteur de texte", + "bool": "Entité du capteur binaire" + }, "device_state": { "add": "Ajouter", "del": "Supprimer", diff --git a/custom_components/xiaomi_home/miot/i18n/it.json b/custom_components/xiaomi_home/miot/i18n/it.json index 7dd652a8..8ec19d3f 100644 --- a/custom_components/xiaomi_home/miot/i18n/it.json +++ b/custom_components/xiaomi_home/miot/i18n/it.json @@ -54,6 +54,10 @@ "enable": "Abilita", "disable": "Disabilita" }, + "binary_mode": { + "text": "Entità Sensore Testo", + "bool": "Entità Sensore Binario" + }, "device_state": { "add": "Aggiungere", "del": "Non disponibile", @@ -149,4 +153,4 @@ "-706014006": "Descrizione del dispositivo non trovata" } } -} +} \ No newline at end of file diff --git a/custom_components/xiaomi_home/miot/i18n/ja.json b/custom_components/xiaomi_home/miot/i18n/ja.json index 087467cd..a32d9971 100644 --- a/custom_components/xiaomi_home/miot/i18n/ja.json +++ b/custom_components/xiaomi_home/miot/i18n/ja.json @@ -54,6 +54,10 @@ "enable": "有効", "disable": "無効" }, + "binary_mode": { + "text": "テキストセンサーエンティティ", + "bool": "バイナリセンサーエンティティ" + }, "device_state": { "add": "追加", "del": "利用不可", diff --git a/custom_components/xiaomi_home/miot/i18n/nl.json b/custom_components/xiaomi_home/miot/i18n/nl.json index d71e90e0..5417348f 100644 --- a/custom_components/xiaomi_home/miot/i18n/nl.json +++ b/custom_components/xiaomi_home/miot/i18n/nl.json @@ -54,6 +54,10 @@ "enable": "Inschakelen", "disable": "Uitschakelen" }, + "binary_mode": { + "text": "Tekstsensor-entiteit", + "bool": "Binairesensor-entiteit" + }, "device_state": { "add": "Toevoegen", "del": "Niet beschikbaar", diff --git a/custom_components/xiaomi_home/miot/i18n/pt-BR.json b/custom_components/xiaomi_home/miot/i18n/pt-BR.json index 0364f7d7..553a90cc 100644 --- a/custom_components/xiaomi_home/miot/i18n/pt-BR.json +++ b/custom_components/xiaomi_home/miot/i18n/pt-BR.json @@ -54,6 +54,10 @@ "enable": "habilitado", "disable": "desabilitado" }, + "binary_mode": { + "text": "Entidade do sensor de texto", + "bool": "Entidade do sensor binário" + }, "device_state": { "add": "adicionar", "del": "indisponível", diff --git a/custom_components/xiaomi_home/miot/i18n/pt.json b/custom_components/xiaomi_home/miot/i18n/pt.json index d02180fb..6466994c 100644 --- a/custom_components/xiaomi_home/miot/i18n/pt.json +++ b/custom_components/xiaomi_home/miot/i18n/pt.json @@ -54,6 +54,10 @@ "enable": "Habilitar", "disable": "Desabilitar" }, + "binary_mode": { + "text": "Entidade do sensor de texto", + "bool": "Entidade do sensor binário" + }, "device_state": { "add": "Adicionar", "del": "Indisponível", diff --git a/custom_components/xiaomi_home/miot/i18n/ru.json b/custom_components/xiaomi_home/miot/i18n/ru.json index 7065c397..b342ca1f 100644 --- a/custom_components/xiaomi_home/miot/i18n/ru.json +++ b/custom_components/xiaomi_home/miot/i18n/ru.json @@ -54,6 +54,10 @@ "enable": "Включить", "disable": "Отключить" }, + "binary_mode": { + "text": "Сущность текстового датчика", + "bool": "Сущность бинарного датчика" + }, "device_state": { "add": "Добавить", "del": "Недоступно", diff --git a/custom_components/xiaomi_home/miot/i18n/zh-Hans.json b/custom_components/xiaomi_home/miot/i18n/zh-Hans.json index 3d47d2a9..ed692541 100644 --- a/custom_components/xiaomi_home/miot/i18n/zh-Hans.json +++ b/custom_components/xiaomi_home/miot/i18n/zh-Hans.json @@ -54,6 +54,10 @@ "enable": "启用", "disable": "禁用" }, + "binary_mode": { + "text": "文本传感器实体", + "bool": "二进制传感器实体" + }, "device_state": { "add": "新增", "del": "不可用", diff --git a/custom_components/xiaomi_home/miot/i18n/zh-Hant.json b/custom_components/xiaomi_home/miot/i18n/zh-Hant.json index 3c541a76..c3547330 100644 --- a/custom_components/xiaomi_home/miot/i18n/zh-Hant.json +++ b/custom_components/xiaomi_home/miot/i18n/zh-Hant.json @@ -54,6 +54,10 @@ "enable": "啟用", "disable": "禁用" }, + "binary_mode": { + "text": "文本傳感器實體", + "bool": "二進制傳感器實體" + }, "device_state": { "add": "新增", "del": "不可用", diff --git a/custom_components/xiaomi_home/miot/miot_client.py b/custom_components/xiaomi_home/miot/miot_client.py index 203c377e..5f690622 100644 --- a/custom_components/xiaomi_home/miot/miot_client.py +++ b/custom_components/xiaomi_home/miot/miot_client.py @@ -59,7 +59,7 @@ from homeassistant.components import zeroconf # pylint: disable=relative-beyond-top-level -from .common import MIoTMatcher +from .common import MIoTMatcher, slugify_did from .const import ( DEFAULT_CTRL_MODE, DEFAULT_INTEGRATION_LANGUAGE, DEFAULT_NICK_NAME, DOMAIN, MIHOME_CERT_EXPIRE_MARGIN, NETWORK_REFRESH_INTERVAL, @@ -150,7 +150,7 @@ class MIoTClient: # Device list update timestamp _device_list_update_ts: int - _sub_source_list: dict[str] + _sub_source_list: dict[str, str] _sub_tree: MIoTMatcher _sub_device_state: dict[str, MipsDeviceState] @@ -169,6 +169,10 @@ class MIoTClient: _show_devices_changed_notify_timer: Optional[asyncio.TimerHandle] # Display devices changed notify _display_devs_notify: list[str] + _display_notify_content_hash: Optional[int] + # Display binary mode + _display_binary_text: bool + _display_binary_bool: bool def __init__( self, @@ -235,6 +239,11 @@ def __init__( self._display_devs_notify = entry_data.get( 'display_devices_changed_notify', ['add', 'del', 'offline']) + self._display_notify_content_hash = None + self._display_binary_text = 'text' in entry_data.get( + 'display_binary_mode', ['text']) + self._display_binary_bool = 'bool' in entry_data.get( + 'display_binary_mode', ['text']) async def init_async(self) -> None: # Load user config and check @@ -469,6 +478,14 @@ def hide_non_standard_entities(self) -> bool: def display_devices_changed_notify(self) -> list[str]: return self._display_devs_notify + @property + def display_binary_text(self) -> bool: + return self._display_binary_text + + @property + def display_binary_bool(self) -> bool: + return self._display_binary_bool + @display_devices_changed_notify.setter def display_devices_changed_notify(self, value: list[str]) -> None: if set(value) == set(self._display_devs_notify): @@ -543,7 +560,8 @@ async def refresh_oauth_info_async(self) -> bool: return True except Exception as err: self.__show_client_error_notify( - message=self._i18n.translate('miot.client.invalid_oauth_info'), + message=self._i18n.translate( + 'miot.client.invalid_oauth_info'), # type: ignore notify_key='oauth_info') _LOGGER.error( 'refresh oauth info error (%s, %s), %s, %s', @@ -586,7 +604,8 @@ async def refresh_user_cert_async(self) -> bool: return True except MIoTClientError as error: self.__show_client_error_notify( - message=self._i18n.translate('miot.client.invalid_cert_info'), + message=self._i18n.translate( + 'miot.client.invalid_cert_info'), # type: ignore notify_key='user_cert') _LOGGER.error( 'refresh user cert error, %s, %s', @@ -872,8 +891,16 @@ async def remove_device_async(self, did: str) -> None: # Update notify self.__request_show_devices_changed_notify() + async def remove_device2_async(self, did_tag: str) -> None: + for did in self._device_list_cache: + d_tag = slugify_did(cloud_server=self._cloud_server, did=did) + if did_tag == d_tag: + await self.remove_device_async(did) + break + def __get_exec_error_with_rc(self, rc: int) -> str: - err_msg: str = self._i18n.translate(key=f'error.common.{rc}') + err_msg: str = self._i18n.translate( + key=f'error.common.{rc}') # type: ignore if not err_msg: err_msg = f'{self._i18n.translate(key="error.common.-10000")}, ' err_msg += f'code={rc}' @@ -1280,7 +1307,7 @@ async def __load_cache_device_async(self) -> None: if not cache_list: self.__show_client_error_notify( message=self._i18n.translate( - 'miot.client.invalid_device_cache'), + 'miot.client.invalid_device_cache'), # type: ignore notify_key='device_cache') raise MIoTClientError('load device list from cache error') else: @@ -1368,7 +1395,8 @@ async def __refresh_cloud_devices_async(self) -> None: home_ids=list(self._entry_data.get('home_selected', {}).keys())) if not result and 'devices' not in result: self.__show_client_error_notify( - message=self._i18n.translate('miot.client.device_cloud_error'), + message=self._i18n.translate( + 'miot.client.device_cloud_error'), # type: ignore notify_key='device_cloud') return else: @@ -1725,13 +1753,14 @@ async def __refresh_props_handler(self) -> None: @final def __show_client_error_notify( - self, message: str, notify_key: str = '' + self, message: Optional[str], notify_key: str = '' ) -> None: if message: + self._persistence_notify( f'{DOMAIN}{self._uid}{self._cloud_server}{notify_key}error', self._i18n.translate( - key='miot.client.xiaomi_home_error_title'), + key='miot.client.xiaomi_home_error_title'), # type: ignore self._i18n.translate( key='miot.client.xiaomi_home_error', replace={ @@ -1739,8 +1768,7 @@ def __show_client_error_notify( 'nick_name', DEFAULT_NICK_NAME), 'uid': self._uid, 'cloud_server': self._cloud_server, - 'message': message - })) + 'message': message})) # type: ignore else: self._persistence_notify( f'{DOMAIN}{self._uid}{self._cloud_server}{notify_key}error', @@ -1806,27 +1834,34 @@ def __show_devices_changed_notify(self) -> None: key='miot.client.device_list_add', replace={ 'count': count_add, - 'message': message_add}) + 'message': message_add}) # type: ignore if 'del' in self._display_devs_notify and count_del: message += self._i18n.translate( key='miot.client.device_list_del', replace={ 'count': count_del, - 'message': message_del}) + 'message': message_del}) # type: ignore if 'offline' in self._display_devs_notify and count_offline: message += self._i18n.translate( key='miot.client.device_list_offline', replace={ 'count': count_offline, - 'message': message_offline}) + 'message': message_offline}) # type: ignore if message != '': + msg_hash = hash(message) + if msg_hash == self._display_notify_content_hash: + # Notify content no change, return + _LOGGER.debug( + 'device list changed notify content no change, return') + return network_status = self._i18n.translate( key='miot.client.network_status_online' if self._network.network_status else 'miot.client.network_status_offline') self._persistence_notify( self.__gen_notify_key('dev_list_changed'), - self._i18n.translate('miot.client.device_list_changed_title'), + self._i18n.translate( + 'miot.client.device_list_changed_title'), # type: ignore self._i18n.translate( key='miot.client.device_list_changed', replace={ @@ -1835,8 +1870,8 @@ def __show_devices_changed_notify(self) -> None: 'uid': self._uid, 'cloud_server': self._cloud_server, 'network_status': network_status, - 'message': message - })) + 'message': message})) # type: ignore + self._display_notify_content_hash = msg_hash _LOGGER.debug( 'show device list changed notify, add %s, del %s, offline %s', count_add, count_del, count_offline) diff --git a/custom_components/xiaomi_home/miot/miot_cloud.py b/custom_components/xiaomi_home/miot/miot_cloud.py index 98d52045..59d0b506 100644 --- a/custom_components/xiaomi_home/miot/miot_cloud.py +++ b/custom_components/xiaomi_home/miot/miot_cloud.py @@ -744,7 +744,7 @@ async def __get_prop_handler(self) -> bool: prop_obj['fut'].set_result(None) if props_req: _LOGGER.info( - 'get prop from cloud failed, %s, %s', len(key), props_req) + 'get prop from cloud failed, %s', props_req) if self._get_prop_list: self._get_prop_timer = self._main_loop.call_later( diff --git a/custom_components/xiaomi_home/miot/miot_device.py b/custom_components/xiaomi_home/miot/miot_device.py index 353b28fe..991e2b13 100644 --- a/custom_components/xiaomi_home/miot/miot_device.py +++ b/custom_components/xiaomi_home/miot/miot_device.py @@ -75,7 +75,7 @@ ) from homeassistant.helpers.entity import DeviceInfo from homeassistant.components.switch import SwitchDeviceClass -from homeassistant.util import slugify + # pylint: disable=relative-beyond-top-level from .specs.specv2entity import ( @@ -85,6 +85,7 @@ SPEC_PROP_TRANS_MAP, SPEC_SERVICE_TRANS_MAP ) +from .common import slugify_name, slugify_did from .const import DOMAIN from .miot_client import MIoTClient from .miot_error import MIoTClientError, MIoTDeviceError @@ -94,7 +95,9 @@ MIoTSpecEvent, MIoTSpecInstance, MIoTSpecProperty, - MIoTSpecService + MIoTSpecService, + MIoTSpecValueList, + MIoTSpecValueRange ) _LOGGER = logging.getLogger(__name__) @@ -142,9 +145,12 @@ class MIoTDevice: _room_id: str _room_name: str - _suggested_area: str + _suggested_area: Optional[str] - _device_state_sub_list: dict[str, Callable[[str, MIoTDeviceState], None]] + _sub_id: int + _device_state_sub_list: dict[str, dict[ + str, Callable[[str, MIoTDeviceState], None]]] + _value_sub_list: dict[str, dict[str, Callable[[dict, Any], None]]] _entity_list: dict[str, list[MIoTEntityData]] _prop_list: dict[str, list[MIoTSpecProperty]] @@ -153,7 +159,7 @@ class MIoTDevice: def __init__( self, miot_client: MIoTClient, - device_info: dict[str, str], + device_info: dict[str, Any], spec_instance: MIoTSpecInstance ) -> None: self.miot_client = miot_client @@ -183,7 +189,9 @@ def __init__( case _: self._suggested_area = None + self._sub_id = 0 self._device_state_sub_list = {} + self._value_sub_list = {} self._entity_list = {} self._prop_list = {} self._event_list = {} @@ -234,36 +242,76 @@ async def action_async(self, siid: int, aiid: int, in_list: list) -> list: def sub_device_state( self, key: str, handler: Callable[[str, MIoTDeviceState], None] - ) -> bool: - self._device_state_sub_list[key] = handler - return True + ) -> int: + self._sub_id += 1 + if key in self._device_state_sub_list: + self._device_state_sub_list[key][str(self._sub_id)] = handler + else: + self._device_state_sub_list[key] = {str(self._sub_id): handler} + return self._sub_id - def unsub_device_state(self, key: str) -> bool: - self._device_state_sub_list.pop(key, None) - return True + def unsub_device_state(self, key: str, sub_id: int) -> None: + sub_list = self._device_state_sub_list.get(key, None) + if sub_list: + sub_list.pop(str(sub_id), None) + if not sub_list: + self._device_state_sub_list.pop(key, None) def sub_property( - 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, - handler_ctx=handler_ctx) + self, handler: Callable[[dict, Any], None], siid: int, piid: int + ) -> int: + key: str = f'p.{siid}.{piid}' + + def _on_prop_changed(params: dict, ctx: Any) -> None: + for handler in self._value_sub_list[key].values(): + handler(params, ctx) + + self._sub_id += 1 + if key in self._value_sub_list: + self._value_sub_list[key][str(self._sub_id)] = handler + else: + self._value_sub_list[key] = {str(self._sub_id): handler} + self.miot_client.sub_prop( + did=self._did, handler=_on_prop_changed, siid=siid, piid=piid) + return self._sub_id - 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 unsub_property(self, siid: int, piid: int, sub_id: int) -> None: + key: str = f'p.{siid}.{piid}' + + sub_list = self._value_sub_list.get(key, None) + if sub_list: + sub_list.pop(str(sub_id), None) + if not sub_list: + self.miot_client.unsub_prop(did=self._did, siid=siid, piid=piid) + self._value_sub_list.pop(key, None) def sub_event( - self, handler: Callable[[dict, Any], None], siid: int = None, - eiid: int = None, handler_ctx: Any = None - ) -> bool: - return self.miot_client.sub_event( - did=self._did, handler=handler, siid=siid, eiid=eiid, - handler_ctx=handler_ctx) + self, handler: Callable[[dict, Any], None], siid: int, eiid: int + ) -> int: + key: str = f'e.{siid}.{eiid}' + + def _on_event_occurred(params: dict, ctx: Any) -> None: + for handler in self._value_sub_list[key].values(): + handler(params, ctx) + + self._sub_id += 1 + if key in self._value_sub_list: + self._value_sub_list[key][str(self._sub_id)] = handler + else: + self._value_sub_list[key] = {str(self._sub_id): handler} + self.miot_client.sub_event( + did=self._did, handler=_on_event_occurred, siid=siid, eiid=eiid) + return self._sub_id - def unsub_event(self, siid: int = None, eiid: int = None) -> bool: - return self.miot_client.unsub_event( - did=self._did, siid=siid, eiid=eiid) + def unsub_event(self, siid: int, eiid: int, sub_id: int) -> None: + key: str = f'e.{siid}.{eiid}' + + sub_list = self._value_sub_list.get(key, None) + if sub_list: + sub_list.pop(str(sub_id), None) + if not sub_list: + self.miot_client.unsub_event(did=self._did, siid=siid, eiid=eiid) + self._value_sub_list.pop(key, None) @property def device_info(self) -> DeviceInfo: @@ -287,11 +335,8 @@ def did(self) -> str: @property def did_tag(self) -> str: - return slugify(f'{self.miot_client.cloud_server}_{self._did}') - - @staticmethod - def gen_did_tag(cloud_server: str, did: str) -> str: - return slugify(f'{cloud_server}_{did}') + return slugify_did( + cloud_server=self.miot_client.cloud_server, did=self._did) def gen_device_entity_id(self, ha_domain: str) -> str: return ( @@ -308,21 +353,24 @@ def gen_prop_entity_id( ) -> str: return ( f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_' - f'{self._model_strs[-1][:20]}_{slugify(spec_name)}_p_{siid}_{piid}') + f'{self._model_strs[-1][:20]}_{slugify_name(spec_name)}' + f'_p_{siid}_{piid}') def gen_event_entity_id( self, ha_domain: str, spec_name: str, siid: int, eiid: int ) -> str: return ( f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_' - f'{self._model_strs[-1][:20]}_{slugify(spec_name)}_e_{siid}_{eiid}') + f'{self._model_strs[-1][:20]}_{slugify_name(spec_name)}' + f'_e_{siid}_{eiid}') def gen_action_entity_id( self, ha_domain: str, spec_name: str, siid: int, aiid: int ) -> str: return ( f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_' - f'{self._model_strs[-1][:20]}_{slugify(spec_name)}_a_{siid}_{aiid}') + f'{self._model_strs[-1][:20]}_{slugify_name(spec_name)}' + f'_a_{siid}_{aiid}') @property def name(self) -> str: @@ -341,14 +389,20 @@ def append_entity(self, entity_data: MIoTEntityData) -> None: self._entity_list[entity_data.platform].append(entity_data) def append_prop(self, prop: MIoTSpecProperty) -> None: + if not prop.platform: + return self._prop_list.setdefault(prop.platform, []) self._prop_list[prop.platform].append(prop) def append_event(self, event: MIoTSpecEvent) -> None: + if not event.platform: + return self._event_list.setdefault(event.platform, []) self._event_list[event.platform].append(event) def append_action(self, action: MIoTSpecAction) -> None: + if not action.platform: + return self._action_list.setdefault(action.platform, []) self._action_list[action.platform].append(action) @@ -507,7 +561,7 @@ def parse_miot_property_entity( if prop_access != (SPEC_PROP_TRANS_MAP[ 'entities'][platform]['access']): return None - if prop.format_ not in SPEC_PROP_TRANS_MAP[ + if prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[ 'entities'][platform]['format']: return None if prop.unit: @@ -560,9 +614,9 @@ def spec_transform(self) -> None: # general conversion if not prop.platform: if prop.writable: - if prop.format_ == 'str': + if prop.format_ == str: prop.platform = 'text' - elif prop.format_ == 'bool': + elif prop.format_ == bool: prop.platform = 'switch' prop.device_class = SwitchDeviceClass.SWITCH elif prop.value_list: @@ -573,9 +627,11 @@ def spec_transform(self) -> None: # Irregular property will not be transformed. pass elif prop.readable or prop.notifiable: - prop.platform = 'sensor' - if prop.platform: - self.append_prop(prop=prop) + if prop.format_ == bool: + prop.platform = 'binary_sensor' + else: + prop.platform = 'sensor' + self.append_prop(prop=prop) # STEP 3.2: event conversion for event in service.events: if event.platform: @@ -703,10 +759,11 @@ def icon_convert(self, spec_unit: str) -> Optional[str]: def __on_device_state_changed( self, did: str, state: MIoTDeviceState, ctx: Any ) -> None: - self._online = state - for key, handler in self._device_state_sub_list.items(): - self.miot_client.main_loop.call_soon_threadsafe( - handler, key, state) + self._online = state == MIoTDeviceState.ONLINE + for key, sub_list in self._device_state_sub_list.items(): + for handler in sub_list.values(): + self.miot_client.main_loop.call_soon_threadsafe( + handler, key, state) class MIoTServiceEntity(Entity): @@ -718,8 +775,11 @@ class MIoTServiceEntity(Entity): _main_loop: asyncio.AbstractEventLoop _prop_value_map: dict[MIoTSpecProperty, Any] + _state_sub_id: int + _value_sub_ids: dict[str, int] - _event_occurred_handler: Callable[[MIoTSpecEvent, dict], None] + _event_occurred_handler: Optional[ + Callable[[MIoTSpecEvent, dict], None]] _prop_changed_subs: dict[ MIoTSpecProperty, Callable[[MIoTSpecProperty, Any], None]] @@ -738,13 +798,15 @@ def __init__( self.entity_data = entity_data self._main_loop = miot_device.miot_client.main_loop self._prop_value_map = {} + self._state_sub_id = 0 + self._value_sub_ids = {} # Gen entity id - if isinstance(entity_data.spec, MIoTSpecInstance): + if isinstance(self.entity_data.spec, MIoTSpecInstance): self.entity_id = miot_device.gen_device_entity_id(DOMAIN) self._attr_name = f' {self.entity_data.spec.description_trans}' - elif isinstance(entity_data.spec, MIoTSpecService): + elif isinstance(self.entity_data.spec, MIoTSpecService): self.entity_id = miot_device.gen_service_entity_id( - DOMAIN, siid=entity_data.spec.iid) + DOMAIN, siid=self.entity_data.spec.iid) self._attr_name = ( f'{"* "if self.entity_data.spec.proprietary else " "}' f'{self.entity_data.spec.description_trans}') @@ -763,7 +825,9 @@ def __init__( self.entity_id) @property - def event_occurred_handler(self) -> Callable[[MIoTSpecEvent, dict], None]: + def event_occurred_handler( + self + ) -> Optional[Callable[[MIoTSpecEvent, dict], None]]: return self._event_occurred_handler @event_occurred_handler.setter @@ -784,25 +848,27 @@ def unsub_prop_changed(self, prop: MIoTSpecProperty) -> None: self._prop_changed_subs.pop(prop, None) @property - def device_info(self) -> dict: + def device_info(self) -> Optional[DeviceInfo]: return self.miot_device.device_info async def async_added_to_hass(self) -> None: state_id = 's.0' if isinstance(self.entity_data.spec, MIoTSpecService): state_id = f's.{self.entity_data.spec.iid}' - self.miot_device.sub_device_state( + self._state_sub_id = self.miot_device.sub_device_state( key=state_id, handler=self.__on_device_state_changed) # Sub prop for prop in self.entity_data.props: if not prop.notifiable and not prop.readable: continue - self.miot_device.sub_property( + key = f'p.{prop.service.iid}.{prop.iid}' + self._value_sub_ids[key] = self.miot_device.sub_property( handler=self.__on_properties_changed, siid=prop.service.iid, piid=prop.iid) # Sub event for event in self.entity_data.events: - self.miot_device.sub_event( + key = f'e.{event.service.iid}.{event.iid}' + self._value_sub_ids[key] = self.miot_device.sub_event( handler=self.__on_event_occurred, siid=event.service.iid, eiid=event.iid) @@ -817,30 +883,39 @@ async def async_will_remove_from_hass(self) -> None: state_id = 's.0' if isinstance(self.entity_data.spec, MIoTSpecService): state_id = f's.{self.entity_data.spec.iid}' - self.miot_device.unsub_device_state(key=state_id) + self.miot_device.unsub_device_state( + key=state_id, sub_id=self._state_sub_id) # Unsub prop for prop in self.entity_data.props: if not prop.notifiable and not prop.readable: continue - self.miot_device.unsub_property( - siid=prop.service.iid, piid=prop.iid) + sub_id = self._value_sub_ids.pop( + f'p.{prop.service.iid}.{prop.iid}', None) + if sub_id: + self.miot_device.unsub_property( + siid=prop.service.iid, piid=prop.iid, sub_id=sub_id) # Unsub event for event in self.entity_data.events: - self.miot_device.unsub_event( - siid=event.service.iid, eiid=event.iid) + sub_id = self._value_sub_ids.pop( + f'e.{event.service.iid}.{event.iid}', None) + if sub_id: + self.miot_device.unsub_event( + siid=event.service.iid, eiid=event.iid, sub_id=sub_id) - def get_map_description(self, map_: dict[int, Any], key: int) -> Any: + def get_map_value( + 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 + def get_map_key( + self, map_: dict[int, Any], value: Any ) -> Optional[int]: if map_ is None: return None - for key, value in map_.items(): - if value == description: + for key, value_ in map_.items(): + if value_ == value: return key return None @@ -999,11 +1074,12 @@ class MIoTPropertyEntity(Entity): service: MIoTSpecService _main_loop: asyncio.AbstractEventLoop - # {'min':int, 'max':int, 'step': int} - _value_range: dict[str, int] + _value_range: Optional[MIoTSpecValueRange] # {Any: Any} - _value_list: dict[Any, Any] + _value_list: Optional[MIoTSpecValueList] _value: Any + _state_sub_id: int + _value_sub_id: int _pending_write_ha_state_timer: Optional[asyncio.TimerHandle] @@ -1015,12 +1091,10 @@ def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None: self.service = spec.service self._main_loop = miot_device.miot_client.main_loop self._value_range = spec.value_range - if spec.value_list: - self._value_list = { - item['value']: item['description'] for item in spec.value_list} - else: - self._value_list = None + self._value_list = spec.value_list self._value = None + self._state_sub_id = 0 + self._value_sub_id = 0 self._pending_write_ha_state_timer = None # Gen entity_id self.entity_id = self.miot_device.gen_prop_entity_id( @@ -1042,16 +1116,16 @@ def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None: self._value_list) @property - def device_info(self) -> dict: + def device_info(self) -> Optional[DeviceInfo]: return self.miot_device.device_info async def async_added_to_hass(self) -> None: # Sub device state changed - self.miot_device.sub_device_state( + self._state_sub_id = self.miot_device.sub_device_state( key=f'{ self.service.iid}.{self.spec.iid}', handler=self.__on_device_state_changed) # Sub value changed - self.miot_device.sub_property( + self._value_sub_id = self.miot_device.sub_property( handler=self.__on_value_changed, siid=self.service.iid, piid=self.spec.iid) # Refresh value @@ -1063,22 +1137,21 @@ async def async_will_remove_from_hass(self) -> None: self._pending_write_ha_state_timer.cancel() self._pending_write_ha_state_timer = None self.miot_device.unsub_device_state( - key=f'{ self.service.iid}.{self.spec.iid}') + key=f'{ self.service.iid}.{self.spec.iid}', + sub_id=self._state_sub_id) self.miot_device.unsub_property( - siid=self.service.iid, piid=self.spec.iid) + siid=self.service.iid, piid=self.spec.iid, + sub_id=self._value_sub_id) - def get_vlist_description(self, value: Any) -> str: + def get_vlist_description(self, value: Any) -> Optional[str]: if not self._value_list: return None - return self._value_list.get(value, None) + return self._value_list.get_description_by_value(value) def get_vlist_value(self, description: str) -> Any: if not self._value_list: return None - for key, value in self._value_list.items(): - if value == description: - return key - return None + return self._value_list.get_value_by_description(description) async def set_property_async(self, value: Any) -> bool: if not self.spec.writable: @@ -1148,9 +1221,10 @@ class MIoTEventEntity(Entity): service: MIoTSpecService _main_loop: asyncio.AbstractEventLoop - _value: Any _attr_event_types: list[str] _arguments_map: dict[int, str] + _state_sub_id: int + _value_sub_id: int def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecEvent) -> None: if miot_device is None or spec is None or spec.service is None: @@ -1159,7 +1233,6 @@ def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecEvent) -> None: self.spec = spec self.service = spec.service self._main_loop = miot_device.miot_client.main_loop - self._value = None # Gen entity_id self.entity_id = self.miot_device.gen_event_entity_id( ha_domain=DOMAIN, spec_name=spec.name, @@ -1177,6 +1250,8 @@ def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecEvent) -> None: self._arguments_map = {} for prop in spec.argument: self._arguments_map[prop.iid] = prop.description_trans + self._state_sub_id = 0 + self._value_sub_id = 0 _LOGGER.info( 'new miot event entity, %s, %s, %s, %s, %s', @@ -1184,29 +1259,31 @@ def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecEvent) -> None: spec.device_class, self.entity_id) @property - def device_info(self) -> dict: + def device_info(self) -> Optional[DeviceInfo]: return self.miot_device.device_info async def async_added_to_hass(self) -> None: # Sub device state changed - self.miot_device.sub_device_state( + self._state_sub_id = self.miot_device.sub_device_state( key=f'event.{ self.service.iid}.{self.spec.iid}', handler=self.__on_device_state_changed) # Sub value changed - self.miot_device.sub_event( + self._value_sub_id = self.miot_device.sub_event( handler=self.__on_event_occurred, siid=self.service.iid, eiid=self.spec.iid) async def async_will_remove_from_hass(self) -> None: self.miot_device.unsub_device_state( - key=f'event.{ self.service.iid}.{self.spec.iid}') + key=f'event.{ self.service.iid}.{self.spec.iid}', + sub_id=self._state_sub_id) self.miot_device.unsub_event( - siid=self.service.iid, eiid=self.spec.iid) + siid=self.service.iid, eiid=self.spec.iid, + sub_id=self._value_sub_id) @abstractmethod def on_event_occurred( - self, name: str, arguments: list[dict[int, Any]] - ): ... + self, name: str, arguments: dict[str, Any] | None = None + ) -> None: ... def __on_event_occurred(self, params: dict, ctx: Any) -> None: _LOGGER.debug('event occurred, %s', params) @@ -1253,11 +1330,11 @@ class MIoTActionEntity(Entity): miot_device: MIoTDevice spec: MIoTSpecAction service: MIoTSpecService - action_platform: str _main_loop: asyncio.AbstractEventLoop _in_map: dict[int, MIoTSpecProperty] _out_map: dict[int, MIoTSpecProperty] + _state_sub_id: int def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecAction) -> None: if miot_device is None or spec is None or spec.service is None: @@ -1265,8 +1342,8 @@ def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecAction) -> None: self.miot_device = miot_device self.spec = spec self.service = spec.service - self.action_platform = 'action' self._main_loop = miot_device.miot_client.main_loop + self._state_sub_id = 0 # Gen entity_id self.entity_id = self.miot_device.gen_action_entity_id( ha_domain=DOMAIN, spec_name=spec.name, @@ -1286,19 +1363,22 @@ def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecAction) -> None: spec.device_class, self.entity_id) @property - def device_info(self) -> dict: + def device_info(self) -> Optional[DeviceInfo]: return self.miot_device.device_info async def async_added_to_hass(self) -> None: - self.miot_device.sub_device_state( - key=f'{self.action_platform}.{ self.service.iid}.{self.spec.iid}', + self._state_sub_id = self.miot_device.sub_device_state( + key=f'a.{ self.service.iid}.{self.spec.iid}', handler=self.__on_device_state_changed) async def async_will_remove_from_hass(self) -> None: self.miot_device.unsub_device_state( - key=f'{self.action_platform}.{ self.service.iid}.{self.spec.iid}') + key=f'a.{ self.service.iid}.{self.spec.iid}', + sub_id=self._state_sub_id) - async def action_async(self, in_list: list = None) -> Optional[list]: + async def action_async( + self, in_list: Optional[list] = None + ) -> Optional[list]: try: return await self.miot_device.miot_client.action_async( did=self.miot_device.did, diff --git a/custom_components/xiaomi_home/miot/miot_mips.py b/custom_components/xiaomi_home/miot/miot_mips.py index 865c44c0..b1bb65be 100644 --- a/custom_components/xiaomi_home/miot/miot_mips.py +++ b/custom_components/xiaomi_home/miot/miot_mips.py @@ -663,7 +663,8 @@ def __on_connect_failed(self, client: Client, user_data: Any) -> None: def __on_disconnect(self, client, user_data, rc, props) -> None: if self._mqtt_state: - self.log_error(f'mips disconnect, {rc}, {props}') + (self.log_info if rc == 0 else self.log_error)( + f'mips disconnect, {rc}, {props}') self._mqtt_state = False if self._mqtt_timer: self._mqtt_timer.cancel() diff --git a/custom_components/xiaomi_home/miot/miot_spec.py b/custom_components/xiaomi_home/miot/miot_spec.py index 33022a1b..67fde7b3 100644 --- a/custom_components/xiaomi_home/miot/miot_spec.py +++ b/custom_components/xiaomi_home/miot/miot_spec.py @@ -46,44 +46,439 @@ MIoT-Spec-V2 parser. """ import asyncio -import json +import os import platform import time -from typing import Any, Optional -from urllib.parse import urlencode -from urllib.request import Request, urlopen +from typing import Any, Optional, Type, Union import logging +from slugify import slugify + # pylint: disable=relative-beyond-top-level from .const import DEFAULT_INTEGRATION_LANGUAGE, SPEC_STD_LIB_EFFECTIVE_TIME +from .common import MIoTHttp, load_yaml_file from .miot_error import MIoTSpecError -from .miot_storage import ( - MIoTStorage, - SpecBoolTranslation, - SpecFilter, - SpecMultiLang) +from .miot_storage import MIoTStorage _LOGGER = logging.getLogger(__name__) -class MIoTSpecBase: +class MIoTSpecValueRange: + """MIoT SPEC value range class.""" + min_: int + max_: int + step: int + + def __init__(self, value_range: Union[dict, list]) -> None: + if isinstance(value_range, dict): + self.load(value_range) + elif isinstance(value_range, list): + self.from_spec(value_range) + else: + raise MIoTSpecError('invalid value range format') + + def load(self, value_range: dict) -> None: + if ( + 'min' not in value_range + or 'max' not in value_range + or 'step' not in value_range + ): + raise MIoTSpecError('invalid value range') + self.min_ = value_range['min'] + self.max_ = value_range['max'] + self.step = value_range['step'] + + def from_spec(self, value_range: list) -> None: + if len(value_range) != 3: + raise MIoTSpecError('invalid value range') + self.min_ = value_range[0] + self.max_ = value_range[1] + self.step = value_range[2] + + def dump(self) -> dict: + return { + 'min': self.min_, + 'max': self.max_, + 'step': self.step + } + + def __str__(self) -> str: + return f'[{self.min_}, {self.max_}, {self.step}' + + +class MIoTSpecValueListItem: + """MIoT SPEC value list item class.""" + # NOTICE: bool type without name + name: str + # Value + value: Any + # Descriptions after multilingual conversion. + description: str + + def __init__(self, item: dict) -> None: + self.load(item) + + def load(self, item: dict) -> None: + if 'value' not in item or 'description' not in item: + raise MIoTSpecError('invalid value list item, %s') + + self.name = item.get('name', None) + self.value = item['value'] + self.description = item['description'] + + @staticmethod + def from_spec(item: dict) -> 'MIoTSpecValueListItem': + if ( + 'name' not in item + or 'value' not in item + or 'description' not in item + ): + raise MIoTSpecError('invalid value list item, %s') + # Slugify name and convert to lower-case. + cache = { + 'name': slugify(text=item['name'], separator='_').lower(), + 'value': item['value'], + 'description': item['description'] + } + return MIoTSpecValueListItem(cache) + + def dump(self) -> dict: + return { + 'name': self.name, + 'value': self.value, + 'description': self.description + } + + def __str__(self) -> str: + return f'{self.name}: {self.value} - {self.description}' + + +class MIoTSpecValueList: + """MIoT SPEC value list class.""" + # pylint: disable=inconsistent-quotes + items: list[MIoTSpecValueListItem] + + def __init__(self, value_list: list[dict]) -> None: + if not isinstance(value_list, list): + raise MIoTSpecError('invalid value list format') + self.items = [] + self.load(value_list) + + @property + def names(self) -> list[str]: + return [item.name for item in self.items] + + @property + def values(self) -> list[Any]: + return [item.value for item in self.items] + + @property + def descriptions(self) -> list[str]: + return [item.description for item in self.items] + + @staticmethod + def from_spec(value_list: list[dict]) -> 'MIoTSpecValueList': + result = MIoTSpecValueList([]) + dup_desc: dict[str, int] = {} + for item in value_list: + # Handle duplicate descriptions. + count = 0 + if item['description'] in dup_desc: + count = dup_desc[item['description']] + count += 1 + dup_desc[item['description']] = count + if count > 1: + item['name'] = f'{item["name"]}_{count}' + item['description'] = f'{item["description"]}_{count}' + + result.items.append(MIoTSpecValueListItem.from_spec(item)) + return result + + def load(self, value_list: list[dict]) -> None: + for item in value_list: + self.items.append(MIoTSpecValueListItem(item)) + + def to_map(self) -> dict: + return {item.value: item.description for item in self.items} + + def get_value_by_description(self, description: str) -> Any: + for item in self.items: + if item.description == description: + return item.value + return None + + def get_description_by_value(self, value: Any) -> Optional[str]: + for item in self.items: + if item.value == value: + return item.description + return None + + def dump(self) -> list: + return [item.dump() for item in self.items] + + +class _SpecStdLib: + """MIoT-Spec-V2 standard library.""" + # pylint: disable=inconsistent-quotes + _lang: str + _devices: dict[str, dict[str, str]] + _services: dict[str, dict[str, str]] + _properties: dict[str, dict[str, str]] + _events: dict[str, dict[str, str]] + _actions: dict[str, dict[str, str]] + _values: dict[str, dict[str, str]] + + def __init__(self, lang: str) -> None: + self._lang = lang + self._devices = {} + self._services = {} + self._properties = {} + self._events = {} + self._actions = {} + self._values = {} + + self._spec_std_lib = None + + def load(self, std_lib: dict[str, dict[str, dict[str, str]]]) -> None: + if ( + not isinstance(std_lib, dict) + or 'devices' not in std_lib + or 'services' not in std_lib + or 'properties' not in std_lib + or 'events' not in std_lib + or 'actions' not in std_lib + or 'values' not in std_lib + ): + return + self._devices = std_lib['devices'] + self._services = std_lib['services'] + self._properties = std_lib['properties'] + self._events = std_lib['events'] + self._actions = std_lib['actions'] + self._values = std_lib['values'] + + def device_translate(self, key: str) -> Optional[str]: + if not self._devices or key not in self._devices: + return None + if self._lang not in self._devices[key]: + return self._devices[key].get( + DEFAULT_INTEGRATION_LANGUAGE, None) + return self._devices[key][self._lang] + + def service_translate(self, key: str) -> Optional[str]: + if not self._services or key not in self._services: + return None + if self._lang not in self._services[key]: + return self._services[key].get( + DEFAULT_INTEGRATION_LANGUAGE, None) + return self._services[key][self._lang] + + def property_translate(self, key: str) -> Optional[str]: + if not self._properties or key not in self._properties: + return None + if self._lang not in self._properties[key]: + return self._properties[key].get( + DEFAULT_INTEGRATION_LANGUAGE, None) + return self._properties[key][self._lang] + + def event_translate(self, key: str) -> Optional[str]: + if not self._events or key not in self._events: + return None + if self._lang not in self._events[key]: + return self._events[key].get( + DEFAULT_INTEGRATION_LANGUAGE, None) + return self._events[key][self._lang] + + def action_translate(self, key: str) -> Optional[str]: + if not self._actions or key not in self._actions: + return None + if self._lang not in self._actions[key]: + return self._actions[key].get( + DEFAULT_INTEGRATION_LANGUAGE, None) + return self._actions[key][self._lang] + + def value_translate(self, key: str) -> Optional[str]: + if not self._values or key not in self._values: + return None + if self._lang not in self._values[key]: + return self._values[key].get( + DEFAULT_INTEGRATION_LANGUAGE, None) + return self._values[key][self._lang] + + def dump(self) -> dict[str, dict[str, dict[str, str]]]: + return { + 'devices': self._devices, + 'services': self._services, + 'properties': self._properties, + 'events': self._events, + 'actions': self._actions, + 'values': self._values + } + + async def refresh_async(self) -> bool: + std_lib_new = await self.__request_from_cloud_async() + if std_lib_new: + self.load(std_lib_new) + return True + return False + + async def __request_from_cloud_async(self) -> Optional[dict]: + std_libs: Optional[dict] = None + for index in range(3): + try: + tasks: list = [] + # Get std lib + for name in [ + 'device', 'service', 'property', 'event', 'action']: + tasks.append(self.__get_template_list( + 'https://miot-spec.org/miot-spec-v2/template/list/' + + name)) + tasks.append(self.__get_property_value()) + # Async request + results = await asyncio.gather(*tasks) + if None in results: + raise MIoTSpecError('init failed, None in result') + std_libs = { + 'devices': results[0], + 'services': results[1], + 'properties': results[2], + 'events': results[3], + 'actions': results[4], + 'values': results[5], + } + # Get external std lib, Power by LM + tasks.clear() + for name in [ + 'device', 'service', 'property', 'event', 'action', + 'property_value']: + tasks.append(MIoTHttp.get_json_async( + 'https://cdn.cnbj1.fds.api.mi-img.com/res-conf/' + f'xiaomi-home/std_ex_{name}.json')) + results = await asyncio.gather(*tasks) + if results[0]: + for key, value in results[0].items(): + if key in std_libs['devices']: + std_libs['devices'][key].update(value) + else: + std_libs['devices'][key] = value + else: + _LOGGER.error('get external std lib failed, devices') + if results[1]: + for key, value in results[1].items(): + if key in std_libs['services']: + std_libs['services'][key].update(value) + else: + std_libs['services'][key] = value + else: + _LOGGER.error('get external std lib failed, services') + if results[2]: + for key, value in results[2].items(): + if key in std_libs['properties']: + std_libs['properties'][key].update(value) + else: + std_libs['properties'][key] = value + else: + _LOGGER.error('get external std lib failed, properties') + if results[3]: + for key, value in results[3].items(): + if key in std_libs['events']: + std_libs['events'][key].update(value) + else: + std_libs['events'][key] = value + else: + _LOGGER.error('get external std lib failed, events') + if results[4]: + for key, value in results[4].items(): + if key in std_libs['actions']: + std_libs['actions'][key].update(value) + else: + std_libs['actions'][key] = value + else: + _LOGGER.error('get external std lib failed, actions') + if results[5]: + for key, value in results[5].items(): + if key in std_libs['values']: + std_libs['values'][key].update(value) + else: + std_libs['values'][key] = value + else: + _LOGGER.error( + 'get external std lib failed, values') + return std_libs + except Exception as err: # pylint: disable=broad-exception-caught + _LOGGER.error( + 'update spec std lib error, retry, %d, %s', index, err) + return None + + async def __get_property_value(self) -> dict: + reply = await MIoTHttp.get_json_async( + url='https://miot-spec.org/miot-spec-v2' + '/normalization/list/property_value') + if reply is None or 'result' not in reply: + raise MIoTSpecError('get property value failed') + result = {} + for item in reply['result']: + if ( + not isinstance(item, dict) + or 'normalization' not in item + or 'description' not in item + or 'proName' not in item + or 'urn' not in item + ): + continue + result[ + f'{item["urn"]}|{item["proName"]}|{item["normalization"]}' + ] = { + 'zh-Hans': item['description'], + 'en': item['normalization'] + } + return result + + async def __get_template_list(self, url: str) -> dict: + reply = await MIoTHttp.get_json_async(url=url) + if reply is None or 'result' not in reply: + raise MIoTSpecError(f'get service failed, {url}') + result: dict = {} + for item in reply['result']: + if ( + not isinstance(item, dict) + or 'type' not in item + or 'description' not in item + ): + continue + if 'zh_cn' in item['description']: + item['description']['zh-Hans'] = item['description'].pop( + 'zh_cn') + if 'zh_hk' in item['description']: + item['description']['zh-Hant'] = item['description'].pop( + 'zh_hk') + item['description'].pop('zh_tw', None) + elif 'zh_tw' in item['description']: + item['description']['zh-Hant'] = item['description'].pop( + 'zh_tw') + result[item['type']] = item['description'] + return result + + +class _MIoTSpecBase: """MIoT SPEC base class.""" iid: int type_: str description: str - description_trans: Optional[str] + description_trans: str proprietary: bool need_filter: bool - name: Optional[str] + name: str # External params - platform: str + platform: Optional[str] device_class: Any state_class: Any - icon: str + icon: Optional[str] external_unit: Any + expression: Optional[str] - spec_id: str + spec_id: int def __init__(self, spec: dict) -> None: self.iid = spec['iid'] @@ -93,44 +488,50 @@ def __init__(self, spec: dict) -> None: self.description_trans = spec.get('description_trans', None) self.proprietary = spec.get('proprietary', False) self.need_filter = spec.get('need_filter', False) - self.name = spec.get('name', None) + self.name = spec.get('name', 'xiaomi') self.platform = None self.device_class = None self.state_class = None self.icon = None self.external_unit = None + self.expression = None self.spec_id = hash(f'{self.type_}.{self.iid}') def __hash__(self) -> int: return self.spec_id - def __eq__(self, value: object) -> bool: + def __eq__(self, value) -> bool: return self.spec_id == value.spec_id -class MIoTSpecProperty(MIoTSpecBase): +class MIoTSpecProperty(_MIoTSpecBase): """MIoT SPEC property class.""" - format_: str + unit: Optional[str] precision: int - unit: str - value_range: list - value_list: list[dict] + _format_: Type + _value_range: Optional[MIoTSpecValueRange] + _value_list: Optional[MIoTSpecValueList] _access: list _writable: bool _readable: bool _notifiable: bool - service: MIoTSpecBase + service: 'MIoTSpecService' def __init__( - self, spec: dict, service: MIoTSpecBase = None, - format_: str = None, access: list = None, - unit: str = None, value_range: list = None, - value_list: list[dict] = None, precision: int = 0 + self, + spec: dict, + service: 'MIoTSpecService', + format_: str, + access: list, + unit: Optional[str] = None, + value_range: Optional[dict] = None, + value_list: Optional[list[dict]] = None, + precision: Optional[int] = None ) -> None: super().__init__(spec=spec) self.service = service @@ -139,11 +540,24 @@ def __init__( self.unit = unit self.value_range = value_range self.value_list = value_list - self.precision = precision + self.precision = precision or 1 self.spec_id = hash( f'p.{self.name}.{self.service.iid}.{self.iid}') + @property + def format_(self) -> Type: + return self._format_ + + @format_.setter + def format_(self, value: str) -> None: + self._format_ = { + 'string': str, + 'str': str, + 'bool': bool, + 'float': float}.get( + value, int) + @property def access(self) -> list: return self._access @@ -168,15 +582,46 @@ def readable(self) -> bool: def notifiable(self): return self._notifiable + @property + def value_range(self) -> Optional[MIoTSpecValueRange]: + return self._value_range + + @value_range.setter + def value_range(self, value: Union[dict, list, None]) -> None: + """Set value-range, precision.""" + if not value: + self._value_range = None + return + self._value_range = MIoTSpecValueRange(value_range=value) + if isinstance(value, list): + self.precision = len(str(value[2]).split( + '.')[1].rstrip('0')) if '.' in str(value[2]) else 0 + + @property + def value_list(self) -> Optional[MIoTSpecValueList]: + return self._value_list + + @value_list.setter + def value_list( + self, value: Union[list[dict], MIoTSpecValueList, None] + ) -> None: + if not value: + self._value_list = None + return + if isinstance(value, list): + self._value_list = MIoTSpecValueList(value_list=value) + elif isinstance(value, MIoTSpecValueList): + self._value_list = value + def value_format(self, value: Any) -> Any: if value is None: return None - if self.format_ == 'int': + if self.format_ == int: return int(value) - if self.format_ == 'float': + if self.format_ == float: return round(value, self.precision) - if self.format_ == 'bool': - return bool(value in [True, 1, 'true', '1']) + if self.format_ == bool: + return bool(value in [True, 1, 'True', 'true', '1']) return value def dump(self) -> dict: @@ -188,26 +633,27 @@ def dump(self) -> dict: 'description_trans': self.description_trans, 'proprietary': self.proprietary, 'need_filter': self.need_filter, - 'format': self.format_, + 'format': self.format_.__name__, 'access': self._access, 'unit': self.unit, - 'value_range': self.value_range, - 'value_list': self.value_list, + 'value_range': ( + self._value_range.dump() if self._value_range else None), + 'value_list': self._value_list.dump() if self._value_list else None, 'precision': self.precision } -class MIoTSpecEvent(MIoTSpecBase): +class MIoTSpecEvent(_MIoTSpecBase): """MIoT SPEC event class.""" argument: list[MIoTSpecProperty] - service: MIoTSpecBase + service: 'MIoTSpecService' def __init__( - self, spec: dict, service: MIoTSpecBase = None, - argument: list[MIoTSpecProperty] = None + self, spec: dict, service: 'MIoTSpecService', + argument: Optional[list[MIoTSpecProperty]] = None ) -> None: super().__init__(spec=spec) - self.argument = argument + self.argument = argument or [] self.service = service self.spec_id = hash( @@ -221,25 +667,25 @@ def dump(self) -> dict: 'description': self.description, 'description_trans': self.description_trans, 'proprietary': self.proprietary, - 'need_filter': self.need_filter, 'argument': [prop.iid for prop in self.argument], + 'need_filter': self.need_filter } -class MIoTSpecAction(MIoTSpecBase): +class MIoTSpecAction(_MIoTSpecBase): """MIoT SPEC action class.""" in_: list[MIoTSpecProperty] out: list[MIoTSpecProperty] - service: MIoTSpecBase + service: 'MIoTSpecService' def __init__( - self, spec: dict, service: MIoTSpecBase = None, - in_: list[MIoTSpecProperty] = None, - out: list[MIoTSpecProperty] = None + self, spec: dict, service: 'MIoTSpecService', + in_: Optional[list[MIoTSpecProperty]] = None, + out: Optional[list[MIoTSpecProperty]] = None ) -> None: super().__init__(spec=spec) - self.in_ = in_ - self.out = out + self.in_ = in_ or [] + self.out = out or [] self.service = service self.spec_id = hash( @@ -252,14 +698,14 @@ def dump(self) -> dict: 'iid': self.iid, 'description': self.description, 'description_trans': self.description_trans, - 'proprietary': self.proprietary, - 'need_filter': self.need_filter, 'in': [prop.iid for prop in self.in_], - 'out': [prop.iid for prop in self.out] + 'out': [prop.iid for prop in self.out], + 'proprietary': self.proprietary, + 'need_filter': self.need_filter } -class MIoTSpecService(MIoTSpecBase): +class MIoTSpecService(_MIoTSpecBase): """MIoT SPEC service class.""" properties: list[MIoTSpecProperty] events: list[MIoTSpecEvent] @@ -280,9 +726,9 @@ def dump(self) -> dict: 'description_trans': self.description_trans, 'proprietary': self.proprietary, 'properties': [prop.dump() for prop in self.properties], - 'need_filter': self.need_filter, 'events': [event.dump() for event in self.events], 'actions': [action.dump() for action in self.actions], + 'need_filter': self.need_filter } @@ -302,8 +748,7 @@ class MIoTSpecInstance: icon: str def __init__( - self, urn: str = None, name: str = None, - description: str = None, description_trans: str = None + self, urn: str, name: str, description: str, description_trans: str ) -> None: self.urn = urn self.name = name @@ -311,12 +756,13 @@ def __init__( self.description_trans = description_trans self.services = [] - def load(self, specs: dict) -> 'MIoTSpecInstance': - self.urn = specs['urn'] - self.name = specs['name'] - self.description = specs['description'] - self.description_trans = specs['description_trans'] - self.services = [] + @staticmethod + def load(specs: dict) -> 'MIoTSpecInstance': + instance = MIoTSpecInstance( + urn=specs['urn'], + name=specs['name'], + description=specs['description'], + description_trans=specs['description_trans']) for service in specs['services']: spec_service = MIoTSpecService(spec=service) for prop in service['properties']: @@ -328,7 +774,7 @@ def load(self, specs: dict) -> 'MIoTSpecInstance': unit=prop['unit'], value_range=prop['value_range'], value_list=prop['value_list'], - precision=prop.get('precision', 0)) + precision=prop.get('precision', None)) spec_service.properties.append(spec_prop) for event in service['events']: spec_event = MIoTSpecEvent( @@ -359,8 +805,8 @@ def load(self, specs: dict) -> 'MIoTSpecInstance': break spec_action.out = out_list spec_service.actions.append(spec_action) - self.services.append(spec_service) - return self + instance.services.append(spec_service) + return instance def dump(self) -> dict: return { @@ -372,183 +818,394 @@ def dump(self) -> dict: } -class SpecStdLib: - """MIoT-Spec-V2 standard library.""" +class _MIoTSpecMultiLang: + """MIoT SPEC multi lang class.""" + # pylint: disable=broad-exception-caught + _DOMAIN: str = 'miot_specs_multi_lang' + _lang: str + _storage: MIoTStorage + _main_loop: asyncio.AbstractEventLoop + + _custom_cache: dict[str, dict] + _current_data: Optional[dict[str, str]] + + def __init__( + self, lang: Optional[str], + storage: MIoTStorage, + loop: Optional[asyncio.AbstractEventLoop] = None + ) -> None: + self._lang = lang or DEFAULT_INTEGRATION_LANGUAGE + self._storage = storage + self._main_loop = loop or asyncio.get_running_loop() + + self._custom_cache = {} + self._current_data = None + + async def set_spec_async(self, urn: str) -> None: + if urn in self._custom_cache: + self._current_data = self._custom_cache[urn] + return + + trans_cache: dict[str, str] = {} + trans_cloud: dict = {} + trans_local: dict = {} + # Get multi lang from cloud + try: + trans_cloud = await self.__get_multi_lang_async(urn) + if self._lang == 'zh-Hans': + # Simplified Chinese + trans_cache = trans_cloud.get('zh_cn', {}) + elif self._lang == 'zh-Hant': + # Traditional Chinese, zh_hk or zh_tw + trans_cache = trans_cloud.get('zh_hk', {}) + if not trans_cache: + trans_cache = trans_cloud.get('zh_tw', {}) + else: + trans_cache = trans_cloud.get(self._lang, {}) + except Exception as err: + trans_cloud = {} + _LOGGER.info('get multi lang from cloud failed, %s, %s', urn, err) + # Get multi lang from local + try: + trans_local = await self._storage.load_async( + domain=self._DOMAIN, name=urn, type_=dict) # type: ignore + if ( + isinstance(trans_local, dict) + and self._lang in trans_local + ): + trans_cache.update(trans_local[self._lang]) + except Exception as err: + trans_local = {} + _LOGGER.info('get multi lang from local failed, %s, %s', urn, err) + # Default language + if not trans_cache: + if trans_cloud and DEFAULT_INTEGRATION_LANGUAGE in trans_cloud: + trans_cache = trans_cloud[DEFAULT_INTEGRATION_LANGUAGE] + if trans_local and DEFAULT_INTEGRATION_LANGUAGE in trans_local: + trans_cache.update( + trans_local[DEFAULT_INTEGRATION_LANGUAGE]) + trans_data: dict[str, str] = {} + for tag, value in trans_cache.items(): + if value is None or value.strip() == '': + continue + # The dict key is like: + # 'service:002:property:001:valuelist:000' or + # 'service:002:property:001' or 'service:002' + strs: list = tag.split(':') + strs_len = len(strs) + if strs_len == 2: + trans_data[f's:{int(strs[1])}'] = value + elif strs_len == 4: + type_ = 'p' if strs[2] == 'property' else ( + 'a' if strs[2] == 'action' else 'e') + trans_data[ + f'{type_}:{int(strs[1])}:{int(strs[3])}' + ] = value + elif strs_len == 6: + trans_data[ + f'v:{int(strs[1])}:{int(strs[3])}:{int(strs[5])}' + ] = value + + self._custom_cache[urn] = trans_data + self._current_data = trans_data + + def translate(self, key: str) -> Optional[str]: + if not self._current_data: + return None + return self._current_data.get(key, None) + + async def __get_multi_lang_async(self, urn: str) -> dict: + res_trans = await MIoTHttp.get_json_async( + url='https://miot-spec.org/instance/v2/multiLanguage', + params={'urn': urn}) + if ( + not isinstance(res_trans, dict) + or 'data' not in res_trans + or not isinstance(res_trans['data'], dict) + ): + raise MIoTSpecError('invalid translation data') + return res_trans['data'] + + +class _SpecBoolTranslation: + """ + Boolean value translation. + """ + _BOOL_TRANS_FILE = 'specs/bool_trans.yaml' + _main_loop: asyncio.AbstractEventLoop _lang: str - _spec_std_lib: Optional[dict[str, dict[str, dict[str, str]]]] + _data: Optional[dict[str, list]] + _data_default: Optional[list[dict]] - def __init__(self, lang: str) -> None: + def __init__( + self, lang: str, loop: Optional[asyncio.AbstractEventLoop] = None + ) -> None: + self._main_loop = loop or asyncio.get_event_loop() self._lang = lang - self._spec_std_lib = None + self._data = None + self._data_default = None - def init(self, std_lib: dict[str, dict[str, str]]) -> None: + async def init_async(self) -> None: + if isinstance(self._data, dict): + return + data = None + self._data = {} + try: + data = await self._main_loop.run_in_executor( + None, load_yaml_file, + os.path.join( + os.path.dirname(os.path.abspath(__file__)), + self._BOOL_TRANS_FILE)) + except Exception as err: # pylint: disable=broad-exception-caught + _LOGGER.error('bool trans, load file error, %s', err) + return + # Check if the file is a valid file if ( - not isinstance(std_lib, dict) - or 'devices' not in std_lib - or 'services' not in std_lib - or 'properties' not in std_lib - or 'events' not in std_lib - or 'actions' not in std_lib - or 'values' not in std_lib + not isinstance(data, dict) + or 'data' not in data + or not isinstance(data['data'], dict) + or 'translate' not in data + or not isinstance(data['translate'], dict) ): + _LOGGER.error('bool trans, valid file') return - self._spec_std_lib = std_lib - def deinit(self) -> None: - self._spec_std_lib = None + if 'default' in data['translate']: + data_default = ( + data['translate']['default'].get(self._lang, None) + or data['translate']['default'].get( + DEFAULT_INTEGRATION_LANGUAGE, None)) + if data_default: + self._data_default = [ + {'value': True, 'description': data_default['true']}, + {'value': False, 'description': data_default['false']} + ] + + for urn, key in data['data'].items(): + if key not in data['translate']: + _LOGGER.error('bool trans, unknown key, %s, %s', urn, key) + continue + trans_data = ( + data['translate'][key].get(self._lang, None) + or data['translate'][key].get( + DEFAULT_INTEGRATION_LANGUAGE, None)) + if trans_data: + self._data[urn] = [ + {'value': True, 'description': trans_data['true']}, + {'value': False, 'description': trans_data['false']} + ] - def device_translate(self, key: str) -> Optional[str]: - if not self._spec_std_lib or key not in self._spec_std_lib['devices']: - return None - if self._lang not in self._spec_std_lib['devices'][key]: - return self._spec_std_lib['devices'][key].get( - DEFAULT_INTEGRATION_LANGUAGE, None) - return self._spec_std_lib['devices'][key][self._lang] + async def deinit_async(self) -> None: + self._data = None + self._data_default = None + + async def translate_async(self, urn: str) -> Optional[list[dict]]: + """ + MUST call init_async() before calling this method. + [ + {'value': True, 'description': 'True'}, + {'value': False, 'description': 'False'} + ] + """ + if not self._data or urn not in self._data: + return self._data_default + return self._data[urn] + + +class _SpecFilter: + """ + MIoT-Spec-V2 filter for entity conversion. + """ + _SPEC_FILTER_FILE = 'specs/spec_filter.yaml' + _main_loop: asyncio.AbstractEventLoop + _data: Optional[dict[str, dict[str, set]]] + _cache: Optional[dict] - def service_translate(self, key: str) -> Optional[str]: - if not self._spec_std_lib or key not in self._spec_std_lib['services']: - return None - if self._lang not in self._spec_std_lib['services'][key]: - return self._spec_std_lib['services'][key].get( - DEFAULT_INTEGRATION_LANGUAGE, None) - return self._spec_std_lib['services'][key][self._lang] + def __init__(self, loop: Optional[asyncio.AbstractEventLoop]) -> None: + self._main_loop = loop or asyncio.get_event_loop() + self._data = None + self._cache = None - def property_translate(self, key: str) -> Optional[str]: + async def init_async(self) -> None: + if isinstance(self._data, dict): + return + filter_data = None + self._data = {} + try: + filter_data = await self._main_loop.run_in_executor( + None, load_yaml_file, + os.path.join( + os.path.dirname(os.path.abspath(__file__)), + self._SPEC_FILTER_FILE)) + except Exception as err: # pylint: disable=broad-exception-caught + _LOGGER.error('spec filter, load file error, %s', err) + return + if not isinstance(filter_data, dict): + _LOGGER.error('spec filter, invalid spec filter content') + return + for values in list(filter_data.values()): + if not isinstance(values, dict): + _LOGGER.error('spec filter, invalid spec filter data') + return + for value in values.values(): + if not isinstance(value, list): + _LOGGER.error('spec filter, invalid spec filter rules') + return + + self._data = filter_data + + async def deinit_async(self) -> None: + self._cache = None + self._data = None + + async def set_spec_spec(self, urn_key: str) -> None: + """MUST call init_async() first.""" + if not self._data: + return + self._cache = self._data.get(urn_key, None) + + def filter_service(self, siid: int) -> bool: + """Filter service by siid. + MUST call init_async() and set_spec_spec() first.""" if ( - not self._spec_std_lib - or key not in self._spec_std_lib['properties'] + self._cache + and 'services' in self._cache + and ( + str(siid) in self._cache['services'] + or '*' in self._cache['services']) ): - return None - if self._lang not in self._spec_std_lib['properties'][key]: - return self._spec_std_lib['properties'][key].get( - DEFAULT_INTEGRATION_LANGUAGE, None) - return self._spec_std_lib['properties'][key][self._lang] + return True - def event_translate(self, key: str) -> Optional[str]: - if not self._spec_std_lib or key not in self._spec_std_lib['events']: - return None - if self._lang not in self._spec_std_lib['events'][key]: - return self._spec_std_lib['events'][key].get( - DEFAULT_INTEGRATION_LANGUAGE, None) - return self._spec_std_lib['events'][key][self._lang] + return False - def action_translate(self, key: str) -> Optional[str]: - if not self._spec_std_lib or key not in self._spec_std_lib['actions']: - return None - if self._lang not in self._spec_std_lib['actions'][key]: - return self._spec_std_lib['actions'][key].get( - DEFAULT_INTEGRATION_LANGUAGE, None) - return self._spec_std_lib['actions'][key][self._lang] + def filter_property(self, siid: int, piid: int) -> bool: + """Filter property by piid. + MUST call init_async() and set_spec_spec() first.""" + if ( + self._cache + and 'properties' in self._cache + and ( + f'{siid}.{piid}' in self._cache['properties'] + or f'{siid}.*' in self._cache['properties']) + ): + return True + return False - def value_translate(self, key: str) -> Optional[str]: - if not self._spec_std_lib or key not in self._spec_std_lib['values']: - return None - if self._lang not in self._spec_std_lib['values'][key]: - return self._spec_std_lib['values'][key].get( - DEFAULT_INTEGRATION_LANGUAGE, None) - return self._spec_std_lib['values'][key][self._lang] + def filter_event(self, siid: int, eiid: int) -> bool: + """Filter event by eiid. + MUST call init_async() and set_spec_spec() first.""" + if ( + self._cache + and 'events' in self._cache + and ( + f'{siid}.{eiid}' in self._cache['events'] + or f'{siid}.*' in self._cache['events'] + ) + ): + return True + return False - def dump(self) -> dict[str, dict[str, str]]: - return self._spec_std_lib + def filter_action(self, siid: int, aiid: int) -> bool: + """"Filter action by aiid. + MUST call init_async() and set_spec_spec() first.""" + if ( + self._cache + and 'actions' in self._cache + and ( + f'{siid}.{aiid}' in self._cache['actions'] + or f'{siid}.*' in self._cache['actions']) + ): + return True + return False class MIoTSpecParser: """MIoT SPEC parser.""" # pylint: disable=inconsistent-quotes VERSION: int = 1 - DOMAIN: str = 'miot_specs' + _DOMAIN: str = 'miot_specs' _lang: str _storage: MIoTStorage _main_loop: asyncio.AbstractEventLoop - _init_done: bool - _ram_cache: dict + _std_lib: _SpecStdLib + _multi_lang: _MIoTSpecMultiLang + _bool_trans: _SpecBoolTranslation + _spec_filter: _SpecFilter - _std_lib: SpecStdLib - _bool_trans: SpecBoolTranslation - _multi_lang: SpecMultiLang - _spec_filter: SpecFilter + _init_done: bool def __init__( - self, lang: str = DEFAULT_INTEGRATION_LANGUAGE, - storage: MIoTStorage = None, + self, lang: Optional[str], + storage: MIoTStorage, loop: Optional[asyncio.AbstractEventLoop] = None ) -> None: - self._lang = lang + self._lang = lang or DEFAULT_INTEGRATION_LANGUAGE self._storage = storage self._main_loop = loop or asyncio.get_running_loop() + self._std_lib = _SpecStdLib(lang=self._lang) + self._multi_lang = _MIoTSpecMultiLang( + lang=self._lang, storage=self._storage, loop=self._main_loop) + self._bool_trans = _SpecBoolTranslation( + lang=self._lang, loop=self._main_loop) + self._spec_filter = _SpecFilter(loop=self._main_loop) self._init_done = False - self._ram_cache = {} - - self._std_lib = SpecStdLib(lang=self._lang) - self._bool_trans = SpecBoolTranslation( - lang=self._lang, loop=self._main_loop) - self._multi_lang = SpecMultiLang(lang=self._lang, loop=self._main_loop) - self._spec_filter = SpecFilter(loop=self._main_loop) async def init_async(self) -> None: if self._init_done is True: return await self._bool_trans.init_async() - await self._multi_lang.init_async() await self._spec_filter.init_async() - std_lib_cache: dict = None - if self._storage: - std_lib_cache: dict = await self._storage.load_async( - domain=self.DOMAIN, name='spec_std_lib', type_=dict) - if ( - isinstance(std_lib_cache, dict) - and 'data' in std_lib_cache - and 'ts' in std_lib_cache - and isinstance(std_lib_cache['ts'], int) - and int(time.time()) - std_lib_cache['ts'] < - SPEC_STD_LIB_EFFECTIVE_TIME - ): - # Use the cache if the update time is less than 14 day - _LOGGER.debug( - 'use local spec std cache, ts->%s', std_lib_cache['ts']) - self._std_lib.init(std_lib_cache['data']) - self._init_done = True - return + std_lib_cache = await self._storage.load_async( + domain=self._DOMAIN, name='spec_std_lib', type_=dict) + if ( + isinstance(std_lib_cache, dict) + and 'data' in std_lib_cache + and 'ts' in std_lib_cache + and isinstance(std_lib_cache['ts'], int) + and int(time.time()) - std_lib_cache['ts'] < + SPEC_STD_LIB_EFFECTIVE_TIME + ): + # Use the cache if the update time is less than 14 day + _LOGGER.debug( + 'use local spec std cache, ts->%s', std_lib_cache['ts']) + self._std_lib.load(std_lib_cache['data']) + self._init_done = True + return # Update spec std lib - spec_lib_new = await self.__request_spec_std_lib_async() - if spec_lib_new: - self._std_lib.init(spec_lib_new) - if self._storage: - if not await self._storage.save_async( - domain=self.DOMAIN, name='spec_std_lib', - data={ - 'data': self._std_lib.dump(), - 'ts': int(time.time()) - } - ): - _LOGGER.error('save spec std lib failed') + if await self._std_lib.refresh_async(): + if not await self._storage.save_async( + domain=self._DOMAIN, name='spec_std_lib', + data={ + 'data': self._std_lib.dump(), + 'ts': int(time.time()) + } + ): + _LOGGER.error('save spec std lib failed') else: - if std_lib_cache: - self._std_lib.init(std_lib_cache['data']) - _LOGGER.error('get spec std lib failed, use local cache') + if isinstance(std_lib_cache, dict) and 'data' in std_lib_cache: + self._std_lib.load(std_lib_cache['data']) + _LOGGER.info('get spec std lib failed, use local cache') else: - _LOGGER.error('get spec std lib failed') + _LOGGER.error('load spec std lib failed') self._init_done = True async def deinit_async(self) -> None: self._init_done = False - self._std_lib.deinit() + # self._std_lib.deinit() await self._bool_trans.deinit_async() - await self._multi_lang.deinit_async() await self._spec_filter.deinit_async() - self._ram_cache.clear() async def parse( self, urn: str, skip_cache: bool = False, - ) -> MIoTSpecInstance: + ) -> Optional[MIoTSpecInstance]: """MUST await init first !!!""" if not skip_cache: cache_result = await self.__cache_get(urn=urn) if isinstance(cache_result, dict): _LOGGER.debug('get from cache, %s', urn) - return MIoTSpecInstance().load(specs=cache_result) + return MIoTSpecInstance.load(specs=cache_result) # Retry three times for index in range(3): try: @@ -562,18 +1219,15 @@ async def refresh_async(self, urn_list: list[str]) -> int: """MUST await init first !!!""" if not urn_list: return False - spec_std_new: dict = await self.__request_spec_std_lib_async() - if spec_std_new: - self._std_lib.init(spec_std_new) - if self._storage: - if not await self._storage.save_async( - domain=self.DOMAIN, name='spec_std_lib', - data={ - 'data': self._std_lib.dump(), - 'ts': int(time.time()) - } - ): - _LOGGER.error('save spec std lib failed') + if await self._std_lib.refresh_async(): + if not await self._storage.save_async( + domain=self._DOMAIN, name='spec_std_lib', + data={ + 'data': self._std_lib.dump(), + 'ts': int(time.time()) + } + ): + _LOGGER.error('save spec std lib failed') else: raise MIoTSpecError('get spec std lib failed') success_count = 0 @@ -585,202 +1239,29 @@ async def refresh_async(self, urn_list: list[str]) -> int: success_count += sum(1 for result in results if result is not None) return success_count - def __http_get( - self, 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) - - async def __http_get_async( - self, url: str, params: dict = None, headers: dict = None - ) -> dict: - return await self._main_loop.run_in_executor( - None, self.__http_get, url, params, headers) - async def __cache_get(self, urn: str) -> Optional[dict]: - if self._storage is not None: - if platform.system() == 'Windows': - urn = urn.replace(':', '_') - return await self._storage.load_async( - domain=self.DOMAIN, name=f'{urn}_{self._lang}', type_=dict) - return self._ram_cache.get(urn, None) + if platform.system() == 'Windows': + urn = urn.replace(':', '_') + return await self._storage.load_async( + domain=self._DOMAIN, + name=f'{urn}_{self._lang}', + type_=dict) # type: ignore async def __cache_set(self, urn: str, data: dict) -> bool: - if self._storage is not None: - if platform.system() == 'Windows': - urn = urn.replace(':', '_') - return await self._storage.save_async( - domain=self.DOMAIN, name=f'{urn}_{self._lang}', data=data) - self._ram_cache[urn] = data - return True - - def __spec_format2dtype(self, format_: str) -> str: - # 'string'|'bool'|'uint8'|'uint16'|'uint32'| - # 'int8'|'int16'|'int32'|'int64'|'float' - return {'string': 'str', 'bool': 'bool', 'float': 'float'}.get( - format_, 'int') - - async def __request_spec_std_lib_async(self) -> Optional[SpecStdLib]: - std_libs: dict = None - for index in range(3): - try: - tasks: list = [] - # Get std lib - for name in [ - 'device', 'service', 'property', 'event', 'action']: - tasks.append(self.__get_template_list( - 'https://miot-spec.org/miot-spec-v2/template/list/' - + name)) - tasks.append(self.__get_property_value()) - # Async request - results = await asyncio.gather(*tasks) - if None in results: - raise MIoTSpecError('init failed, None in result') - std_libs = { - 'devices': results[0], - 'services': results[1], - 'properties': results[2], - 'events': results[3], - 'actions': results[4], - 'values': results[5], - } - # Get external std lib, Power by LM - tasks.clear() - for name in [ - 'device', 'service', 'property', 'event', 'action', - 'property_value']: - tasks.append(self.__http_get_async( - 'https://cdn.cnbj1.fds.api.mi-img.com/res-conf/' - f'xiaomi-home/std_ex_{name}.json')) - results = await asyncio.gather(*tasks) - if results[0]: - for key, value in results[0].items(): - if key in std_libs['devices']: - std_libs['devices'][key].update(value) - else: - std_libs['devices'][key] = value - else: - _LOGGER.error('get external std lib failed, devices') - if results[1]: - for key, value in results[1].items(): - if key in std_libs['services']: - std_libs['services'][key].update(value) - else: - std_libs['services'][key] = value - else: - _LOGGER.error('get external std lib failed, services') - if results[2]: - for key, value in results[2].items(): - if key in std_libs['properties']: - std_libs['properties'][key].update(value) - else: - std_libs['properties'][key] = value - else: - _LOGGER.error('get external std lib failed, properties') - if results[3]: - for key, value in results[3].items(): - if key in std_libs['events']: - std_libs['events'][key].update(value) - else: - std_libs['events'][key] = value - else: - _LOGGER.error('get external std lib failed, events') - if results[4]: - for key, value in results[4].items(): - if key in std_libs['actions']: - std_libs['actions'][key].update(value) - else: - std_libs['actions'][key] = value - else: - _LOGGER.error('get external std lib failed, actions') - if results[5]: - for key, value in results[5].items(): - if key in std_libs['values']: - std_libs['values'][key].update(value) - else: - std_libs['values'][key] = value - else: - _LOGGER.error( - 'get external std lib failed, values') - return std_libs - except Exception as err: # pylint: disable=broad-exception-caught - _LOGGER.error( - 'update spec std lib error, retry, %d, %s', index, err) - return None - - async def __get_property_value(self) -> dict: - reply = await self.__http_get_async( - url='https://miot-spec.org/miot-spec-v2' - '/normalization/list/property_value') - if reply is None or 'result' not in reply: - raise MIoTSpecError('get property value failed') - result = {} - for item in reply['result']: - if ( - not isinstance(item, dict) - or 'normalization' not in item - or 'description' not in item - or 'proName' not in item - or 'urn' not in item - ): - continue - result[ - f'{item["urn"]}|{item["proName"]}|{item["normalization"]}' - ] = { - 'zh-Hans': item['description'], - 'en': item['normalization'] - } - return result + if platform.system() == 'Windows': + urn = urn.replace(':', '_') + return await self._storage.save_async( + domain=self._DOMAIN, name=f'{urn}_{self._lang}', data=data) - async def __get_template_list(self, url: str) -> dict: - reply = await self.__http_get_async(url=url) - if reply is None or 'result' not in reply: - raise MIoTSpecError(f'get service failed, {url}') - result: dict = {} - for item in reply['result']: - if ( - not isinstance(item, dict) - or 'type' not in item - or 'description' not in item - ): - continue - if 'zh_cn' in item['description']: - item['description']['zh-Hans'] = item['description'].pop( - 'zh_cn') - if 'zh_hk' in item['description']: - item['description']['zh-Hant'] = item['description'].pop( - 'zh_hk') - item['description'].pop('zh_tw', None) - elif 'zh_tw' in item['description']: - item['description']['zh-Hant'] = item['description'].pop( - 'zh_tw') - result[item['type']] = item['description'] - return result - - async def __get_instance(self, urn: str) -> dict: - return await self.__http_get_async( + async def __get_instance(self, urn: str) -> Optional[dict]: + return await MIoTHttp.get_json_async( url='https://miot-spec.org/miot-spec-v2/instance', params={'type': urn}) - async def __get_translation(self, urn: str) -> dict: - return await self.__http_get_async( - url='https://miot-spec.org/instance/v2/multiLanguage', - params={'urn': urn}) - async def __parse(self, urn: str) -> MIoTSpecInstance: _LOGGER.debug('parse urn, %s', urn) # Load spec instance - instance: dict = await self.__get_instance(urn=urn) + instance = await self.__get_instance(urn=urn) if ( not isinstance(instance, dict) or 'type' not in instance @@ -788,69 +1269,12 @@ async def __parse(self, urn: str) -> MIoTSpecInstance: or 'services' not in instance ): raise MIoTSpecError(f'invalid urn instance, {urn}') - translation: dict = {} - try: - # Load multiple language configuration. - res_trans = await self.__get_translation(urn=urn) - if ( - not isinstance(res_trans, dict) - or 'data' not in res_trans - or not isinstance(res_trans['data'], dict) - ): - raise MIoTSpecError('invalid translation data') - urn_strs: list[str] = urn.split(':') - urn_key: str = ':'.join(urn_strs[:6]) - trans_data: dict[str, str] = None - if self._lang == 'zh-Hans': - # Simplified Chinese - trans_data = res_trans['data'].get('zh_cn', {}) - elif self._lang == 'zh-Hant': - # Traditional Chinese, zh_hk or zh_tw - trans_data = res_trans['data'].get('zh_hk', {}) - if not trans_data: - trans_data = res_trans['data'].get('zh_tw', {}) - else: - trans_data = res_trans['data'].get(self._lang, {}) - # Load local multiple language configuration. - multi_lang: dict = await self._multi_lang.translate_async( - urn_key=urn_key) - if multi_lang: - trans_data.update(multi_lang) - if not trans_data: - trans_data = res_trans['data'].get( - DEFAULT_INTEGRATION_LANGUAGE, {}) - if not trans_data: - raise MIoTSpecError( - f'the language is not supported, {self._lang}') - else: - _LOGGER.error( - 'the language is not supported, %s, try using the ' - 'default language, %s, %s', - self._lang, DEFAULT_INTEGRATION_LANGUAGE, urn) - for tag, value in trans_data.items(): - if value is None or value.strip() == '': - continue - # The dict key is like: - # 'service:002:property:001:valuelist:000' or - # 'service:002:property:001' or 'service:002' - strs: list = tag.split(':') - strs_len = len(strs) - if strs_len == 2: - translation[f's:{int(strs[1])}'] = value - elif strs_len == 4: - type_ = 'p' if strs[2] == 'property' else ( - 'a' if strs[2] == 'action' else 'e') - translation[ - f'{type_}:{int(strs[1])}:{int(strs[3])}' - ] = value - elif strs_len == 6: - translation[ - f'v:{int(strs[1])}:{int(strs[3])}:{int(strs[5])}' - ] = value - except MIoTSpecError as e: - _LOGGER.error('get translation error, %s, %s', urn, e) - # Spec filter - self._spec_filter.filter_spec(urn_key=urn_key) + urn_strs: list[str] = urn.split(':') + urn_key: str = ':'.join(urn_strs[:6]) + # Set translation cache + await self._multi_lang.set_spec_async(urn=urn) + # Set spec filter + await self._spec_filter.set_spec_spec(urn_key=urn_key) # Parse device type spec_instance: MIoTSpecInstance = MIoTSpecInstance( urn=urn, name=urn_strs[3], @@ -880,7 +1304,7 @@ async def __parse(self, urn: str) -> MIoTSpecInstance: if type_strs[1] != 'miot-spec-v2': spec_service.proprietary = True spec_service.description_trans = ( - translation.get(f's:{service["iid"]}', None) + self._multi_lang.translate(f's:{service["iid"]}') or self._std_lib.service_translate(key=':'.join(type_strs[:5])) or service['description'] or spec_service.name @@ -899,7 +1323,7 @@ async def __parse(self, urn: str) -> MIoTSpecInstance: spec_prop: MIoTSpecProperty = MIoTSpecProperty( spec=property_, service=spec_service, - format_=self.__spec_format2dtype(property_['format']), + format_=property_['format'], access=property_['access'], unit=property_.get('unit', None)) spec_prop.name = p_type_strs[3] @@ -911,41 +1335,35 @@ async def __parse(self, urn: str) -> MIoTSpecInstance: if p_type_strs[1] != 'miot-spec-v2': spec_prop.proprietary = spec_service.proprietary or True spec_prop.description_trans = ( - translation.get( - f'p:{service["iid"]}:{property_["iid"]}', None) + self._multi_lang.translate( + f'p:{service["iid"]}:{property_["iid"]}') or self._std_lib.property_translate( key=':'.join(p_type_strs[:5])) or property_['description'] or spec_prop.name) if 'value-range' in property_: - spec_prop.value_range = { - 'min': property_['value-range'][0], - 'max': property_['value-range'][1], - 'step': property_['value-range'][2] - } - spec_prop.precision = len(str( - property_['value-range'][2]).split( - '.')[1].rstrip('0')) if '.' in str( - property_['value-range'][2]) else 0 + spec_prop.value_range = property_['value-range'] elif 'value-list' in property_: v_list: list[dict] = property_['value-list'] for index, v in enumerate(v_list): + if v['description'].strip() == '': + v['description'] = f'v_{v["value"]}' v['name'] = v['description'] v['description'] = ( - translation.get( + self._multi_lang.translate( f'v:{service["iid"]}:{property_["iid"]}:' - f'{index}', None) + f'{index}') or self._std_lib.value_translate( key=f'{type_strs[:5]}|{p_type_strs[3]}|' f'{v["description"]}') - or v['name'] - ) - spec_prop.value_list = v_list + or v['name']) + spec_prop.value_list = MIoTSpecValueList.from_spec(v_list) elif property_['format'] == 'bool': v_tag = ':'.join(p_type_strs[:5]) - v_descriptions: dict = ( + v_descriptions = ( await self._bool_trans.translate_async(urn=v_tag)) if v_descriptions: + # bool without value-list.name spec_prop.value_list = v_descriptions spec_service.properties.append(spec_prop) # Parse service event @@ -969,8 +1387,8 @@ async def __parse(self, urn: str) -> MIoTSpecInstance: if e_type_strs[1] != 'miot-spec-v2': spec_event.proprietary = spec_service.proprietary or True spec_event.description_trans = ( - translation.get( - f'e:{service["iid"]}:{event["iid"]}', None) + self._multi_lang.translate( + f'e:{service["iid"]}:{event["iid"]}') or self._std_lib.event_translate( key=':'.join(e_type_strs[:5])) or event['description'] @@ -1005,8 +1423,8 @@ async def __parse(self, urn: str) -> MIoTSpecInstance: if a_type_strs[1] != 'miot-spec-v2': spec_action.proprietary = spec_service.proprietary or True spec_action.description_trans = ( - translation.get( - f'a:{service["iid"]}:{action["iid"]}', None) + self._multi_lang.translate( + f'a:{service["iid"]}:{action["iid"]}') or self._std_lib.action_translate( key=':'.join(a_type_strs[:5])) or action['description'] diff --git a/custom_components/xiaomi_home/miot/miot_storage.py b/custom_components/xiaomi_home/miot/miot_storage.py index 85b25c98..3767365e 100644 --- a/custom_components/xiaomi_home/miot/miot_storage.py +++ b/custom_components/xiaomi_home/miot/miot_storage.py @@ -58,7 +58,6 @@ from pathlib import Path from typing import Any, Optional, Union import logging -from urllib.request import Request, urlopen from cryptography.hazmat.primitives import serialization from cryptography.hazmat.backends import default_backend from cryptography.x509.oid import NameOID @@ -66,13 +65,13 @@ from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ed25519 + # pylint: disable=relative-beyond-top-level -from .common import load_json_file from .const import ( - DEFAULT_INTEGRATION_LANGUAGE, MANUFACTURER_EFFECTIVE_TIME, MIHOME_CA_CERT_STR, MIHOME_CA_CERT_SHA256) +from .common import MIoTHttp from .miot_error import MIoTCertError, MIoTError, MIoTStorageError _LOGGER = logging.getLogger(__name__) @@ -93,10 +92,10 @@ class MIoTStorage: User data will be stored in the `.storage` directory of Home Assistant. """ - _main_loop: asyncio.AbstractEventLoop = None + _main_loop: asyncio.AbstractEventLoop _file_future: dict[str, tuple[MIoTStorageType, asyncio.Future]] - _root_path: str = None + _root_path: str def __init__( self, root_path: str, @@ -140,7 +139,7 @@ def __load( if r_data is None: _LOGGER.error('load error, empty file, %s', full_path) return None - data_bytes: bytes = None + data_bytes: bytes # Hash check if with_hash_check: if len(r_data) <= 32: @@ -209,17 +208,17 @@ def __save( else: os.makedirs(os.path.dirname(full_path), exist_ok=True) try: - type_: type = type(data) - w_bytes: bytes = None - if type_ == bytes: + w_bytes: bytes + if isinstance(data, bytes): w_bytes = data - elif type_ == str: + elif isinstance(data, str): w_bytes = data.encode('utf-8') - elif type_ in [dict, list]: + elif isinstance(data, (dict, list)): w_bytes = json.dumps(data).encode('utf-8') else: _LOGGER.error( - 'save error, unsupported data type, %s', type_.__name__) + 'save error, unsupported data type, %s', + type(data).__name__) return False with open(full_path, 'wb') as w_file: w_file.write(w_bytes) @@ -353,7 +352,8 @@ async def save_file_async( def load_file(self, domain: str, name_with_suffix: str) -> Optional[bytes]: full_path = os.path.join(self._root_path, domain, name_with_suffix) return self.__load( - full_path=full_path, type_=bytes, with_hash_check=False) + full_path=full_path, type_=bytes, + with_hash_check=False) # type: ignore async def load_file_async( self, domain: str, name_with_suffix: str @@ -371,7 +371,7 @@ async def load_file_async( None, self.__load, full_path, bytes, False) if not fut.done(): self.__add_file_future(full_path, MIoTStorageType.LOAD_FILE, fut) - return await fut + return await fut # type: ignore def remove_file(self, domain: str, name_with_suffix: str) -> bool: full_path = os.path.join(self._root_path, domain, name_with_suffix) @@ -438,7 +438,7 @@ def update_user_config( domain=config_domain, name=config_name, data=config) local_config = (self.load(domain=config_domain, name=config_name, type_=dict)) or {} - local_config.update(config) + local_config.update(config) # type: ignore return self.save( domain=config_domain, name=config_name, data=local_config) @@ -474,27 +474,31 @@ async def update_user_config_async( domain=config_domain, name=config_name, data=config) local_config = (await self.load_async( domain=config_domain, name=config_name, type_=dict)) or {} - local_config.update(config) + local_config.update(config) # type: ignore return await self.save_async( domain=config_domain, name=config_name, data=local_config) def load_user_config( self, uid: str, cloud_server: str, keys: Optional[list[str]] = None ) -> dict[str, Any]: - if keys is not None and len(keys) == 0: + if isinstance(keys, list) and len(keys) == 0: # Do nothing return {} config_domain = 'miot_config' config_name = f'{uid}_{cloud_server}' local_config = (self.load(domain=config_domain, - name=config_name, type_=dict)) or {} + name=config_name, type_=dict)) + if not isinstance(local_config, dict): + return {} if keys is None: return local_config - return {key: local_config.get(key, None) for key in keys} + return { + key: local_config[key] for key in keys + if key in local_config} async def load_user_config_async( self, uid: str, cloud_server: str, keys: Optional[list[str]] = None - ) -> dict[str, Any]: + ) -> dict: """Load user configuration. Args: @@ -505,13 +509,15 @@ async def load_user_config_async( Returns: dict[str, Any]: query result """ - if keys is not None and len(keys) == 0: + if isinstance(keys, list) and len(keys) == 0: # Do nothing return {} config_domain = 'miot_config' config_name = f'{uid}_{cloud_server}' local_config = (await self.load_async( - domain=config_domain, name=config_name, type_=dict)) or {} + domain=config_domain, name=config_name, type_=dict)) + if not isinstance(local_config, dict): + return {} if keys is None: return local_config return { @@ -519,7 +525,8 @@ async def load_user_config_async( if key in local_config} def gen_storage_path( - self, domain: str = None, name_with_suffix: str = None + self, domain: Optional[str] = None, + name_with_suffix: Optional[str] = None ) -> str: """Generate file path.""" result = self._root_path @@ -609,9 +616,8 @@ async def user_cert_remaining_time_async( if cert_data is None: return 0 # Check user cert - user_cert: x509.Certificate = None try: - user_cert = x509.load_pem_x509_certificate( + user_cert: x509.Certificate = x509.load_pem_x509_certificate( cert_data, default_backend()) cert_info = {} for attribute in user_cert.subject: @@ -669,7 +675,8 @@ def gen_user_csr(self, user_key: str, did: str) -> str: NameOID.COMMON_NAME, f'mips.{self._uid}.{did_hash}.2'), ])) csr = builder.sign( - private_key, algorithm=None, backend=default_backend()) + private_key, algorithm=None, # type: ignore + backend=default_backend()) return csr.public_bytes(serialization.Encoding.PEM).decode('utf-8') async def load_user_key_async(self) -> Optional[str]: @@ -719,250 +726,6 @@ def __did_hash(self, did: str) -> str: return binascii.hexlify(sha1_hash.finalize()).decode('utf-8') -class SpecMultiLang: - """ - MIoT-Spec-V2 multi-language for entities. - """ - MULTI_LANG_FILE = 'specs/multi_lang.json' - _main_loop: asyncio.AbstractEventLoop - _lang: str - _data: Optional[dict[str, dict]] - - def __init__( - self, lang: str, loop: Optional[asyncio.AbstractEventLoop] = None - ) -> None: - self._main_loop = loop or asyncio.get_event_loop() - self._lang = lang - self._data = None - - async def init_async(self) -> None: - if isinstance(self._data, dict): - return - multi_lang_data = None - self._data = {} - try: - multi_lang_data = await self._main_loop.run_in_executor( - None, load_json_file, - os.path.join( - os.path.dirname(os.path.abspath(__file__)), - self.MULTI_LANG_FILE)) - except Exception as err: # pylint: disable=broad-exception-caught - _LOGGER.error('multi lang, load file error, %s', err) - return - # Check if the file is a valid JSON file - if not isinstance(multi_lang_data, dict): - _LOGGER.error('multi lang, invalid file data') - return - for lang_data in multi_lang_data.values(): - if not isinstance(lang_data, dict): - _LOGGER.error('multi lang, invalid lang data') - return - for data in lang_data.values(): - if not isinstance(data, dict): - _LOGGER.error('multi lang, invalid lang data item') - return - self._data = multi_lang_data - - async def deinit_async(self) -> str: - self._data = None - - async def translate_async(self, urn_key: str) -> dict[str, str]: - """MUST call init_async() first.""" - if urn_key in self._data: - return self._data[urn_key].get(self._lang, {}) - return {} - - -class SpecBoolTranslation: - """ - Boolean value translation. - """ - BOOL_TRANS_FILE = 'specs/bool_trans.json' - _main_loop: asyncio.AbstractEventLoop - _lang: str - _data: Optional[dict[str, dict]] - _data_default: dict[str, dict] - - def __init__( - self, lang: str, loop: Optional[asyncio.AbstractEventLoop] = None - ) -> None: - self._main_loop = loop or asyncio.get_event_loop() - self._lang = lang - self._data = None - - async def init_async(self) -> None: - if isinstance(self._data, dict): - return - data = None - self._data = {} - try: - data = await self._main_loop.run_in_executor( - None, load_json_file, - os.path.join( - os.path.dirname(os.path.abspath(__file__)), - self.BOOL_TRANS_FILE)) - except Exception as err: # pylint: disable=broad-exception-caught - _LOGGER.error('bool trans, load file error, %s', err) - return - # Check if the file is a valid JSON file - if ( - not isinstance(data, dict) - or 'data' not in data - or not isinstance(data['data'], dict) - or 'translate' not in data - or not isinstance(data['translate'], dict) - ): - _LOGGER.error('bool trans, valid file') - return - - if 'default' in data['translate']: - data_default = ( - data['translate']['default'].get(self._lang, None) - or data['translate']['default'].get( - DEFAULT_INTEGRATION_LANGUAGE, None)) - if data_default: - self._data_default = [ - {'value': True, 'description': data_default['true']}, - {'value': False, 'description': data_default['false']} - ] - - for urn, key in data['data'].items(): - if key not in data['translate']: - _LOGGER.error('bool trans, unknown key, %s, %s', urn, key) - continue - trans_data = ( - data['translate'][key].get(self._lang, None) - or data['translate'][key].get( - DEFAULT_INTEGRATION_LANGUAGE, None)) - if trans_data: - self._data[urn] = [ - {'value': True, 'description': trans_data['true']}, - {'value': False, 'description': trans_data['false']} - ] - - async def deinit_async(self) -> None: - self._data = None - self._data_default = None - - async def translate_async(self, urn: str) -> list[dict[bool, str]]: - """ - MUST call init_async() before calling this method. - [ - {'value': True, 'description': 'True'}, - {'value': False, 'description': 'False'} - ] - """ - - return self._data.get(urn, self._data_default) - - -class SpecFilter: - """ - MIoT-Spec-V2 filter for entity conversion. - """ - SPEC_FILTER_FILE = 'specs/spec_filter.json' - _main_loop: asyncio.AbstractEventLoop - _data: dict[str, dict[str, set]] - _cache: Optional[dict] - - def __init__(self, loop: Optional[asyncio.AbstractEventLoop]) -> None: - self._main_loop = loop or asyncio.get_event_loop() - self._data = None - self._cache = None - - async def init_async(self) -> None: - if isinstance(self._data, dict): - return - filter_data = None - self._data = {} - try: - filter_data = await self._main_loop.run_in_executor( - None, load_json_file, - os.path.join( - os.path.dirname(os.path.abspath(__file__)), - self.SPEC_FILTER_FILE)) - except Exception as err: # pylint: disable=broad-exception-caught - _LOGGER.error('spec filter, load file error, %s', err) - return - if not isinstance(filter_data, dict): - _LOGGER.error('spec filter, invalid spec filter content') - return - for values in list(filter_data.values()): - if not isinstance(values, dict): - _LOGGER.error('spec filter, invalid spec filter data') - return - for value in values.values(): - if not isinstance(value, list): - _LOGGER.error('spec filter, invalid spec filter rules') - return - - self._data = filter_data - - async def deinit_async(self) -> None: - self._cache = None - self._data = None - - def filter_spec(self, urn_key: str) -> None: - """MUST call init_async() first.""" - if not self._data: - return - self._cache = self._data.get(urn_key, None) - - def filter_service(self, siid: int) -> bool: - """Filter service by siid. - MUST call init_async() and filter_spec() first.""" - if ( - self._cache - and 'services' in self._cache - and ( - str(siid) in self._cache['services'] - or '*' in self._cache['services']) - ): - return True - - return False - - def filter_property(self, siid: int, piid: int) -> bool: - """Filter property by piid. - MUST call init_async() and filter_spec() first.""" - if ( - self._cache - and 'properties' in self._cache - and ( - f'{siid}.{piid}' in self._cache['properties'] - or f'{siid}.*' in self._cache['properties']) - ): - return True - return False - - def filter_event(self, siid: int, eiid: int) -> bool: - """Filter event by eiid. - MUST call init_async() and filter_spec() first.""" - if ( - self._cache - and 'events' in self._cache - and ( - f'{siid}.{eiid}' in self._cache['events'] - or f'{siid}.*' in self._cache['events'] - ) - ): - return True - return False - - def filter_action(self, siid: int, aiid: int) -> bool: - """"Filter action by aiid. - MUST call init_async() and filter_spec() first.""" - if ( - self._cache - and 'actions' in self._cache - and ( - f'{siid}.{aiid}' in self._cache['actions'] - or f'{siid}.*' in self._cache['actions']) - ): - return True - return False - - class DeviceManufacturer: """Device manufacturer.""" DOMAIN: str = 'miot_specs' @@ -976,12 +739,11 @@ def __init__( ) -> None: self._main_loop = loop or asyncio.get_event_loop() self._storage = storage - self._data = None + self._data = {} async def init_async(self) -> None: if self._data: return - data_cache: dict = None data_cache = await self._storage.load_async( domain=self.DOMAIN, name='manufacturer', type_=dict) if ( @@ -995,8 +757,15 @@ async def init_async(self) -> None: _LOGGER.debug('load manufacturer data success') return - data_cloud = await self._main_loop.run_in_executor( - None, self.__get_manufacturer_data) + data_cloud = None + try: + data_cloud = await MIoTHttp.get_json_async( + url='https://cdn.cnbj1.fds.api.mi-img.com/res-conf/xiaomi-home/' + 'manufacturer.json', + loop=self._main_loop) + except Exception as err: # pylint: disable=broad-exception-caught + _LOGGER.error('get manufacturer info failed, %s', err) + if data_cloud: await self._storage.save_async( domain=self.DOMAIN, name='manufacturer', @@ -1004,32 +773,16 @@ async def init_async(self) -> None: self._data = data_cloud _LOGGER.debug('update manufacturer data success') else: - if data_cache: - self._data = data_cache.get('data', None) + if isinstance(data_cache, dict): + self._data = data_cache.get('data', {}) _LOGGER.error('load manufacturer data failed, use local data') else: _LOGGER.error('load manufacturer data failed') async def deinit_async(self) -> None: - self._data = None + self._data.clear() def get_name(self, short_name: str) -> str: if not self._data or not short_name or short_name not in self._data: return short_name return self._data[short_name].get('name', None) or short_name - - def __get_manufacturer_data(self) -> dict: - try: - request = Request( - 'https://cdn.cnbj1.fds.api.mi-img.com/res-conf/xiaomi-home/' - 'manufacturer.json', - method='GET') - content: bytes = None - with urlopen(request) as response: - content = response.read() - return ( - json.loads(str(content, 'utf-8')) - if content else None) - except Exception as err: # pylint: disable=broad-exception-caught - _LOGGER.error('get manufacturer info failed, %s', err) - return None diff --git a/custom_components/xiaomi_home/miot/specs/bool_trans.json b/custom_components/xiaomi_home/miot/specs/bool_trans.json deleted file mode 100644 index 4bee0b4d..00000000 --- a/custom_components/xiaomi_home/miot/specs/bool_trans.json +++ /dev/null @@ -1,315 +0,0 @@ -{ - "data": { - "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: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", - "urn:miot-spec-v2:property:guard-mode:000000B6": "open_close", - "urn:miot-spec-v2:property:heater:00000026": "open_close", - "urn:miot-spec-v2:property:heating:000000C7": "open_close", - "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: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", - "urn:miot-spec-v2:property:valve-switch:000000FE": "open_close", - "urn:miot-spec-v2:property:ventilation:000000CE": "open_close", - "urn:miot-spec-v2:property:vertical-swing:00000018": "open_close", - "urn:miot-spec-v2:property:wake-up-mode:00000107": "open_close", - "urn:miot-spec-v2:property:water-pump:000000F2": "open_close", - "urn:miot-spec-v2:property:watering:000000CC": "open_close", - "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:wifi-ssid-hidden:000000E3": "yes_no", - "urn:miot-spec-v2:property:wind-reverse:00000117": "yes_no" - }, - "translate": { - "default": { - "de": { - "true": "Wahr", - "false": "Falsch" - }, - "en": { - "true": "True", - "false": "False" - }, - "es": { - "true": "Verdadero", - "false": "Falso" - }, - "fr": { - "true": "Vrai", - "false": "Faux" - }, - "it": { - "true": "Vero", - "false": "Falso" - }, - "ja": { - "true": "真", - "false": "偽" - }, - "nl": { - "true": "True", - "false": "False" - }, - "pt": { - "true": "True", - "false": "False" - }, - "pt-BR": { - "true": "True", - "false": "False" - }, - "ru": { - "true": "Истина", - "false": "Ложь" - }, - "zh-Hans": { - "true": "真", - "false": "假" - }, - "zh-Hant": { - "true": "真", - "false": "假" - } - }, - "open_close": { - "de": { - "true": "Öffnen", - "false": "Schließen" - }, - "en": { - "true": "Open", - "false": "Close" - }, - "es": { - "true": "Abierto", - "false": "Cerrado" - }, - "fr": { - "true": "Ouvert", - "false": "Fermer" - }, - "it": { - "true": "Aperto", - "false": "Chiuso" - }, - "ja": { - "true": "開く", - "false": "閉じる" - }, - "nl": { - "true": "Open", - "false": "Dicht" - }, - "pt": { - "true": "Aberto", - "false": "Fechado" - }, - "pt-BR": { - "true": "Aberto", - "false": "Fechado" - }, - "ru": { - "true": "Открыть", - "false": "Закрыть" - }, - "zh-Hans": { - "true": "开启", - "false": "关闭" - }, - "zh-Hant": { - "true": "開啟", - "false": "關閉" - } - }, - "yes_no": { - "de": { - "true": "Ja", - "false": "Nein" - }, - "en": { - "true": "Yes", - "false": "No" - }, - "es": { - "true": "Sí", - "false": "No" - }, - "fr": { - "true": "Oui", - "false": "Non" - }, - "it": { - "true": "Si", - "false": "No" - }, - "ja": { - "true": "はい", - "false": "いいえ" - }, - "nl": { - "true": "Ja", - "false": "Nee" - }, - "pt": { - "true": "Sim", - "false": "Não" - }, - "pt-BR": { - "true": "Sim", - "false": "Não" - }, - "ru": { - "true": "Да", - "false": "Нет" - }, - "zh-Hans": { - "true": "是", - "false": "否" - }, - "zh-Hant": { - "true": "是", - "false": "否" - } - }, - "motion_state": { - "de": { - "true": "Bewegung erkannt", - "false": "Keine Bewegung erkannt" - }, - "en": { - "true": "Motion Detected", - "false": "No Motion Detected" - }, - "es": { - "true": "Movimiento detectado", - "false": "No se detecta movimiento" - }, - "fr": { - "true": "Mouvement détecté", - "false": "Aucun mouvement détecté" - }, - "it": { - "true": "Movimento Rilevato", - "false": "Nessun Movimento Rilevato" - }, - "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": "Движение не обнаружено" - }, - "zh-Hans": { - "true": "有人", - "false": "无人" - }, - "zh-Hant": { - "true": "有人", - "false": "無人" - } - }, - "contact_state": { - "de": { - "true": "Kontakt", - "false": "Kein Kontakt" - }, - "en": { - "true": "Contact", - "false": "No Contact" - }, - "es": { - "true": "Contacto", - "false": "Sin contacto" - }, - "fr": { - "true": "Contact", - "false": "Pas de contact" - }, - "it": { - "true": "Contatto", - "false": "Nessun Contatto" - }, - "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": "Нет контакта" - }, - "zh-Hans": { - "true": "接触", - "false": "分离" - }, - "zh-Hant": { - "true": "接觸", - "false": "分離" - } - } - } -} diff --git a/custom_components/xiaomi_home/miot/specs/bool_trans.yaml b/custom_components/xiaomi_home/miot/specs/bool_trans.yaml new file mode 100644 index 00000000..7bb3483b --- /dev/null +++ b/custom_components/xiaomi_home/miot/specs/bool_trans.yaml @@ -0,0 +1,246 @@ +data: + 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: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 + urn:miot-spec-v2:property:guard-mode:000000B6: open_close + urn:miot-spec-v2:property:heater:00000026: open_close + urn:miot-spec-v2:property:heating:000000C7: open_close + 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: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 + urn:miot-spec-v2:property:valve-switch:000000FE: open_close + urn:miot-spec-v2:property:ventilation:000000CE: open_close + urn:miot-spec-v2:property:vertical-swing:00000018: open_close + urn:miot-spec-v2:property:wake-up-mode:00000107: open_close + urn:miot-spec-v2:property:water-pump:000000F2: open_close + urn:miot-spec-v2:property:watering:000000CC: open_close + 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:wifi-ssid-hidden:000000E3: yes_no + urn:miot-spec-v2:property:wind-reverse:00000117: yes_no +translate: + contact_state: + de: + 'false': Kein Kontakt + 'true': Kontakt + en: + 'false': No Contact + 'true': Contact + es: + 'false': Sin contacto + 'true': Contacto + fr: + 'false': Pas de contact + 'true': Contact + it: + 'false': Nessun contatto + 'true': Contatto + ja: + 'false': 非接触 + 'true': 接触 + nl: + 'false': Geen contact + 'true': Contact + pt: + 'false': Sem contato + 'true': Contato + pt-BR: + 'false': Sem contato + 'true': Contato + ru: + 'false': Нет контакта + 'true': Контакт + zh-Hans: + 'false': 分离 + 'true': 接触 + zh-Hant: + 'false': 分離 + 'true': 接觸 + default: + de: + 'false': Falsch + 'true': Wahr + en: + 'false': 'False' + 'true': 'True' + es: + 'false': Falso + 'true': Verdadero + fr: + 'false': Faux + 'true': Vrai + it: + 'false': Falso + 'true': Vero + ja: + 'false': 偽 + 'true': 真 + nl: + 'false': 'False' + 'true': 'True' + pt: + 'false': 'False' + 'true': 'True' + pt-BR: + 'false': 'False' + 'true': 'True' + ru: + 'false': Ложь + 'true': Истина + zh-Hans: + 'false': 假 + 'true': 真 + zh-Hant: + 'false': 假 + 'true': 真 + motion_state: + de: + 'false': Keine Bewegung erkannt + 'true': Bewegung erkannt + en: + 'false': No Motion Detected + 'true': Motion Detected + es: + 'false': No se detecta movimiento + 'true': Movimiento detectado + fr: + 'false': Aucun mouvement détecté + 'true': Mouvement détecté + it: + 'false': Nessun Movimento Rilevato + 'true': Movimento Rilevato + ja: + 'false': 動きが検出されません + 'true': 動きを検知 + nl: + 'false': Geen contact + 'true': Contact + pt: + 'false': Sem contato + 'true': Contato + pt-BR: + 'false': Sem contato + 'true': Contato + ru: + 'false': Движение не обнаружено + 'true': Обнаружено движение + zh-Hans: + 'false': 无人 + 'true': 有人 + zh-Hant: + 'false': 無人 + 'true': 有人 + open_close: + de: + 'false': Schließen + 'true': Öffnen + en: + 'false': Close + 'true': Open + es: + 'false': Cerrado + 'true': Abierto + fr: + 'false': Fermer + 'true': Ouvert + it: + 'false': Chiuso + 'true': Aperto + ja: + 'false': 閉じる + 'true': 開く + nl: + 'false': Dicht + 'true': Open + pt: + 'false': Fechado + 'true': Aberto + pt-BR: + 'false': Fechado + 'true': Aberto + ru: + 'false': Закрыть + 'true': Открыть + zh-Hans: + 'false': 关闭 + 'true': 开启 + zh-Hant: + 'false': 關閉 + 'true': 開啟 + yes_no: + de: + 'false': Nein + 'true': Ja + en: + 'false': 'No' + 'true': 'Yes' + es: + 'false': 'No' + 'true': Sí + fr: + 'false': Non + 'true': Oui + it: + 'false': 'No' + 'true': Si + ja: + 'false': いいえ + 'true': はい + nl: + 'false': Nee + 'true': Ja + pt: + 'false': Não + 'true': Sim + pt-BR: + 'false': Não + 'true': Sim + ru: + 'false': Нет + 'true': Да + zh-Hans: + 'false': 否 + 'true': 是 + zh-Hant: + 'false': 否 + 'true': 是 diff --git a/custom_components/xiaomi_home/miot/specs/multi_lang.json b/custom_components/xiaomi_home/miot/specs/multi_lang.json deleted file mode 100644 index 8cd42840..00000000 --- a/custom_components/xiaomi_home/miot/specs/multi_lang.json +++ /dev/null @@ -1,194 +0,0 @@ -{ - "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", - "service:001:property:005": "Serial Number (SN)", - "service:002": "Gateway", - "service:002:event:001": "Network Changed", - "service:002:event:002": "Network Changed", - "service:002:property:001": "Access Method", - "service:002:property:001:valuelist:000": "Wired", - "service:002:property:001:valuelist:001": "5G Wireless", - "service:002:property:001:valuelist:002": "2.4G Wireless", - "service:002:property:002": "IP Address", - "service:002:property:003": "WiFi Network Name", - "service:002:property:004": "Current Time", - "service:002:property:005": "DHCP Server MAC Address", - "service:003": "Indicator Light", - "service:003:property:001": "Switch", - "service:004": "Virtual Service", - "service:004:action:001": "Generate Virtual Event", - "service:004:event:001": "Virtual Event Occurred", - "service:004:property:001": "Event Name" - }, - "es": { - "service:001": "Información del dispositivo", - "service:001:property:003": "ID del dispositivo", - "service:001:property:005": "Número de serie (SN)", - "service:002": "Puerta de enlace", - "service:002:event:001": "Cambio de red", - "service:002:event:002": "Cambio de red", - "service:002:property:001": "Método de acceso", - "service:002:property:001:valuelist:000": "Cableado", - "service:002:property:001:valuelist:001": "5G inalámbrico", - "service:002:property:001:valuelist:002": "2.4G inalámbrico", - "service:002:property:002": "Dirección IP", - "service:002:property:003": "Nombre de red WiFi", - "service:002:property:004": "Hora actual", - "service:002:property:005": "Dirección MAC del servidor DHCP", - "service:003": "Luz indicadora", - "service:003:property:001": "Interruptor", - "service:004": "Servicio virtual", - "service:004:action:001": "Generar evento virtual", - "service:004:event:001": "Ocurrió un evento virtual", - "service:004:property:001": "Nombre del evento" - }, - "fr": { - "service:001": "Informations sur l'appareil", - "service:001:property:003": "ID de l'appareil", - "service:001:property:005": "Numéro de série (SN)", - "service:002": "Passerelle", - "service:002:event:001": "Changement de réseau", - "service:002:event:002": "Changement de réseau", - "service:002:property:001": "Méthode d'accès", - "service:002:property:001:valuelist:000": "Câblé", - "service:002:property:001:valuelist:001": "Sans fil 5G", - "service:002:property:001:valuelist:002": "Sans fil 2.4G", - "service:002:property:002": "Adresse IP", - "service:002:property:003": "Nom du réseau WiFi", - "service:002:property:004": "Heure actuelle", - "service:002:property:005": "Adresse MAC du serveur DHCP", - "service:003": "Voyant lumineux", - "service:003:property:001": "Interrupteur", - "service:004": "Service virtuel", - "service:004:action:001": "Générer un événement virtuel", - "service:004:event:001": "Événement virtuel survenu", - "service:004:property:001": "Nom de l'événement" - }, - "it": { - "service:001": "Informazioni sul Dispositivo", - "service:001:property:003": "ID Dispositivo", - "service:001:property:005": "Numero di Serie (SN)", - "service:002": "Gateway", - "service:002:event:001": "Rete Modificata", - "service:002:event:002": "Rete Modificata", - "service:002:property:001": "Metodo di Accesso", - "service:002:property:001:valuelist:000": "Cablato", - "service:002:property:001:valuelist:001": "Wireless 5G", - "service:002:property:001:valuelist:002": "Wireless 2.4G", - "service:002:property:002": "Indirizzo IP", - "service:002:property:003": "Nome Rete WiFi", - "service:002:property:004": "Ora Attuale", - "service:002:property:005": "Indirizzo MAC del Server DHCP", - "service:003": "Luce Indicatore", - "service:003:property:001": "Interruttore", - "service:004": "Servizio Virtuale", - "service:004:action:001": "Genera Evento Virtuale", - "service:004:event:001": "Evento Virtuale Avvenuto", - "service:004:property:001": "Nome Evento" - }, - "ja": { - "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": "DHCPサーバーMACアドレス", - "service:003": "インジケータライト", - "service:003:property:001": "スイッチ", - "service:004": "バーチャルサービス", - "service:004:action:001": "バーチャルイベントを生成", - "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", - "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": "DHCP伺服器MAC地址", - "service:003": "指示燈", - "service:003:property:001": "開關", - "service:004": "虛擬服務", - "service:004:action:001": "產生虛擬事件", - "service:004:event:001": "虛擬事件發生", - "service:004:property:001": "事件名稱" - } - }, - "urn:miot-spec-v2:device:switch:0000A003:lumi-acn040:1": { - "en": { - "service:011": "Right Button On and Off", - "service:011:property:001": "Right Button On and Off", - "service:015:action:001": "Left Button Identify", - "service:016:action:001": "Middle Button Identify", - "service:017:action:001": "Right Button Identify" - }, - "zh-Hans": { - "service:015:action:001": "左键确认", - "service:016:action:001": "中键确认", - "service:017:action:001": "右键确认" - } - } -} diff --git a/custom_components/xiaomi_home/miot/specs/spec_filter.json b/custom_components/xiaomi_home/miot/specs/spec_filter.json deleted file mode 100644 index 274fb342..00000000 --- a/custom_components/xiaomi_home/miot/specs/spec_filter.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4": { - "properties": [ - "9.*", - "13.*", - "15.*" - ], - "services": [ - "10" - ] - }, - "urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01": { - "properties": [ - "5.1" - ], - "services": [ - "4", - "7", - "8" - ] - }, - "urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": { - "events": [ - "2.1" - ] - }, - "urn:miot-spec-v2:device:health-pot:0000A051:chunmi-a1": { - "services": [ - "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.*", - "2.5" - ] - }, - "urn:miot-spec-v2:device:light:0000A001:yeelink-dnlight2": { - "services": [ - "3" - ] - }, - "urn:miot-spec-v2:device:light:0000A001:yeelink-mbulb3": { - "services": [ - "3" - ] - }, - "urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1": { - "services": [ - "1", - "5" - ] - }, - "urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03": { - "services": [ - "*" - ] - } -} \ No newline at end of file diff --git a/custom_components/xiaomi_home/miot/specs/spec_filter.yaml b/custom_components/xiaomi_home/miot/specs/spec_filter.yaml new file mode 100644 index 00000000..2102e48c --- /dev/null +++ b/custom_components/xiaomi_home/miot/specs/spec_filter.yaml @@ -0,0 +1,43 @@ +urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4: + properties: + - 9.* + - 13.* + - 15.* + services: + - '10' +urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01: + properties: + - '5.1' + services: + - '4' + - '7' + - '8' +urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1: + events: + - '2.1' +urn:miot-spec-v2:device:health-pot:0000A051:chunmi-a1: + services: + - '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.* + - '2.5' +urn:miot-spec-v2:device:light:0000A001:yeelink-dnlight2: + services: + - '3' +urn:miot-spec-v2:device:light:0000A001:yeelink-mbulb3: + services: + - '3' +urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1: + services: + - '1' + - '5' +urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03: + services: + - '*' diff --git a/custom_components/xiaomi_home/notify.py b/custom_components/xiaomi_home/notify.py index ba0844a5..5cf3fd87 100644 --- a/custom_components/xiaomi_home/notify.py +++ b/custom_components/xiaomi_home/notify.py @@ -90,7 +90,7 @@ def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecAction) -> None: super().__init__(miot_device=miot_device, spec=spec) self._attr_extra_state_attributes = {} action_in: str = ', '.join([ - f'{prop.description_trans}({prop.format_})' + f'{prop.description_trans}({prop.format_.__name__})' for prop in self.spec.in_]) self._attr_extra_state_attributes['action params'] = f'[{action_in}]' @@ -122,24 +122,24 @@ async def async_send_message( return in_value: list[dict] = [] for index, prop in enumerate(self.spec.in_): - if prop.format_ == 'str': + if prop.format_ == str: if isinstance(in_list[index], (bool, int, float, str)): in_value.append( {'piid': prop.iid, 'value': str(in_list[index])}) continue - elif prop.format_ == 'bool': + elif prop.format_ == bool: if isinstance(in_list[index], (bool, int)): # yes, no, on, off, true, false and other bool types # will also be parsed as 0 and 1 of int. in_value.append( {'piid': prop.iid, 'value': bool(in_list[index])}) continue - elif prop.format_ == 'float': + elif prop.format_ == float: if isinstance(in_list[index], (int, float)): in_value.append( {'piid': prop.iid, 'value': in_list[index]}) continue - elif prop.format_ == 'int': + elif prop.format_ == int: if isinstance(in_list[index], int): in_value.append( {'piid': prop.iid, 'value': in_list[index]}) diff --git a/custom_components/xiaomi_home/number.py b/custom_components/xiaomi_home/number.py index 53bc09c6..29bd6b78 100644 --- a/custom_components/xiaomi_home/number.py +++ b/custom_components/xiaomi_home/number.py @@ -92,9 +92,9 @@ def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None: self._attr_icon = self.spec.icon # Set value range if self._value_range: - self._attr_native_min_value = self._value_range['min'] - self._attr_native_max_value = self._value_range['max'] - self._attr_native_step = self._value_range['step'] + self._attr_native_min_value = self._value_range.min_ + self._attr_native_max_value = self._value_range.max_ + self._attr_native_step = self._value_range.step @property def native_value(self) -> Optional[float]: diff --git a/custom_components/xiaomi_home/select.py b/custom_components/xiaomi_home/select.py index 4c9bad3e..21b5e784 100644 --- a/custom_components/xiaomi_home/select.py +++ b/custom_components/xiaomi_home/select.py @@ -82,7 +82,8 @@ class Select(MIoTPropertyEntity, SelectEntity): def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None: """Initialize the Select.""" super().__init__(miot_device=miot_device, spec=spec) - self._attr_options = list(self._value_list.values()) + if self._value_list: + self._attr_options = self._value_list.descriptions async def async_select_option(self, option: str) -> None: """Change the selected option.""" diff --git a/custom_components/xiaomi_home/sensor.py b/custom_components/xiaomi_home/sensor.py index 39b3bdb2..88b4bacf 100644 --- a/custom_components/xiaomi_home/sensor.py +++ b/custom_components/xiaomi_home/sensor.py @@ -76,6 +76,12 @@ async def async_setup_entry( for prop in miot_device.prop_list.get('sensor', []): new_entities.append(Sensor(miot_device=miot_device, spec=prop)) + if miot_device.miot_client.display_binary_text: + for prop in miot_device.prop_list.get('binary_sensor', []): + if not prop.value_list: + continue + new_entities.append(Sensor(miot_device=miot_device, spec=prop)) + if new_entities: async_add_entities(new_entities) @@ -91,7 +97,7 @@ def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None: self._attr_device_class = SensorDeviceClass.ENUM self._attr_icon = 'mdi:message-text' self._attr_native_unit_of_measurement = None - self._attr_options = list(self._value_list.values()) + self._attr_options = self._value_list.descriptions else: self._attr_device_class = spec.device_class if spec.external_unit: @@ -100,29 +106,29 @@ def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None: # device_class is not empty but unit is empty. # Set the default unit according to device_class. unit_sets = DEVICE_CLASS_UNITS.get( - self._attr_device_class, None) + self._attr_device_class, None) # type: ignore self._attr_native_unit_of_measurement = list( unit_sets)[0] if unit_sets else None + # Set state_class + if spec.state_class: + self._attr_state_class = spec.state_class # Set icon if spec.icon: self._attr_icon = spec.icon - # Set state_class - if spec.state_class: - self._attr_state_class = spec.state_class @property def native_value(self) -> Any: """Return the current value of the sensor.""" if self._value_range and isinstance(self._value, (int, float)): if ( - self._value < self._value_range['min'] - or self._value > self._value_range['max'] + self._value < self._value_range.min_ + or self._value > self._value_range.max_ ): _LOGGER.info( '%s, data exception, out of range, %s, %s', self.entity_id, self._value, self._value_range) if self._value_list: - return self._value_list.get(self._value, None) + return self.get_vlist_description(self._value) if isinstance(self._value, str): return self._value[:255] return self._value diff --git a/custom_components/xiaomi_home/text.py b/custom_components/xiaomi_home/text.py index 8a6b9aef..ff6ac3e6 100644 --- a/custom_components/xiaomi_home/text.py +++ b/custom_components/xiaomi_home/text.py @@ -76,9 +76,10 @@ async def async_setup_entry( for prop in miot_device.prop_list.get('text', []): new_entities.append(Text(miot_device=miot_device, spec=prop)) - for action in miot_device.action_list.get('action_text', []): - new_entities.append(ActionText( - miot_device=miot_device, spec=action)) + if miot_device.miot_client.action_debug: + for action in miot_device.action_list.get('notify', []): + new_entities.append(ActionText( + miot_device=miot_device, spec=action)) if new_entities: async_add_entities(new_entities) @@ -111,11 +112,9 @@ def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecAction) -> None: self._attr_extra_state_attributes = {} self._attr_native_value = '' action_in: str = ', '.join([ - f'{prop.description_trans}({prop.format_})' + f'{prop.description_trans}({prop.format_.__name__})' for prop in self.spec.in_]) self._attr_extra_state_attributes['action params'] = f'[{action_in}]' - # For action debug - self.action_platform = 'action_text' async def async_set_value(self, value: str) -> None: if not value: @@ -141,24 +140,24 @@ async def async_set_value(self, value: str) -> None: f'invalid action params, {value}') in_value: list[dict] = [] for index, prop in enumerate(self.spec.in_): - if prop.format_ == 'str': + if prop.format_ == str: if isinstance(in_list[index], (bool, int, float, str)): in_value.append( {'piid': prop.iid, 'value': str(in_list[index])}) continue - elif prop.format_ == 'bool': + elif prop.format_ == bool: if isinstance(in_list[index], (bool, int)): # yes, no, on, off, true, false and other bool types # will also be parsed as 0 and 1 of int. in_value.append( {'piid': prop.iid, 'value': bool(in_list[index])}) continue - elif prop.format_ == 'float': + elif prop.format_ == float: if isinstance(in_list[index], (int, float)): in_value.append( {'piid': prop.iid, 'value': in_list[index]}) continue - elif prop.format_ == 'int': + elif prop.format_ == int: if isinstance(in_list[index], int): in_value.append( {'piid': prop.iid, 'value': in_list[index]}) diff --git a/custom_components/xiaomi_home/translations/de.json b/custom_components/xiaomi_home/translations/de.json index 25dfd023..15f5c0fa 100644 --- a/custom_components/xiaomi_home/translations/de.json +++ b/custom_components/xiaomi_home/translations/de.json @@ -42,12 +42,13 @@ }, "advanced_options": { "title": "Erweiterte Einstellungen", - "description": "## Gebrauchsanweisung\r\n### Wenn Sie die Bedeutung der folgenden Optionen nicht genau kennen, belassen Sie sie bitte auf den Standardeinstellungen.\r\n### Geräte filtern\r\nUnterstützt das Filtern von Geräten nach Raumnamen und Gerätetypen sowie das Filtern nach Gerätedimensionen.\r\n\r\n### Steuerungsmodus\r\n- Automatisch: Wenn ein verfügbarer Xiaomi-Hub im lokalen Netzwerk vorhanden ist, priorisiert Home Assistant das Senden von Steuerbefehlen über den Hub, um eine lokale Steuerung zu ermöglichen. Wenn kein Hub vorhanden ist, wird versucht, Steuerbefehle über das Xiaomi-OT-Protokoll zu senden. Nur wenn diese Bedingungen für die lokale Steuerung nicht erfüllt sind, werden die Befehle über die Cloud gesendet.\r\n- Cloud: Steuerbefehle werden ausschließlich über die Cloud gesendet.\r\n### Action-Debug-Modus\r\nFür Methoden, die von MIoT-Spec-V2-Geräten definiert werden, wird neben der Benachrichtigungsentität auch eine Texteingabe-Entität erstellt, mit der Sie während des Debuggens Steuerbefehle an das Gerät senden können.\r\n### Nicht standardmäßige Entitäten ausblenden\r\nBlendet Entitäten aus, die von nicht-standardmäßigen MIoT-Spec-V2-Instanzen generiert werden und deren Name mit „*“ beginnt.\r\n### Gerätestatusänderungen anzeigen\r\nDetaillierte Anzeige von Gerätestatusänderungen, es werden nur die ausgewählten Benachrichtigungen angezeigt.", + "description": "## Gebrauchsanweisung\r\n### Wenn Sie die Bedeutung der folgenden Optionen nicht genau kennen, belassen Sie sie bitte auf den Standardeinstellungen.\r\n### Geräte filtern\r\nUnterstützt das Filtern von Geräten nach Raumnamen und Gerätetypen sowie das Filtern nach Gerätedimensionen.\r\n\r\n### Steuerungsmodus\r\n- Automatisch: Wenn ein verfügbarer Xiaomi-Hub im lokalen Netzwerk vorhanden ist, priorisiert Home Assistant das Senden von Steuerbefehlen über den Hub, um eine lokale Steuerung zu ermöglichen. Wenn kein Hub vorhanden ist, wird versucht, Steuerbefehle über das Xiaomi-OT-Protokoll zu senden. Nur wenn diese Bedingungen für die lokale Steuerung nicht erfüllt sind, werden die Befehle über die Cloud gesendet.\r\n- Cloud: Steuerbefehle werden ausschließlich über die Cloud gesendet.\r\n### Action-Debug-Modus\r\nFür Methoden, die von MIoT-Spec-V2-Geräten definiert werden, wird neben der Benachrichtigungsentität auch eine Texteingabe-Entität erstellt, mit der Sie während des Debuggens Steuerbefehle an das Gerät senden können.\r\n### Nicht standardmäßige Entitäten ausblenden\r\nBlendet Entitäten aus, die von nicht-standardmäßigen MIoT-Spec-V2-Instanzen generiert werden und deren Name mit „*“ beginnt.\r\n### Binärsensor-Anzeigemodus\r\nZeigt Binärsensoren in Xiaomi Home als Textsensor-Entität oder Binärsensor-Entität an。\r\n### Gerätestatusänderungen anzeigen\r\nDetaillierte Anzeige von Gerätestatusänderungen, es werden nur die ausgewählten Benachrichtigungen angezeigt.", "data": { "devices_filter": "Geräte filtern", "ctrl_mode": "Steuerungsmodus", "action_debug": "Action-Debug-Modus", "hide_non_standard_entities": "Nicht standardmäßige Entitäten ausblenden", + "display_binary_mode": "Binärsensor-Anzeigemodus", "display_devices_changed_notify": "Gerätestatusänderungen anzeigen" } }, @@ -119,6 +120,7 @@ "update_devices": "Geräteliste aktualisieren", "action_debug": "Action-Debug-Modus", "hide_non_standard_entities": "Verstecke Nicht-Standard-Entitäten", + "display_binary_mode": "Binärsensor-Anzeigemodus", "display_devices_changed_notify": "Gerätestatusänderungen anzeigen", "update_trans_rules": "Entitätskonvertierungsregeln aktualisieren", "update_lan_ctrl_config": "LAN-Steuerungskonfiguration aktualisieren", diff --git a/custom_components/xiaomi_home/translations/en.json b/custom_components/xiaomi_home/translations/en.json index 0ee151c1..7832fd1b 100644 --- a/custom_components/xiaomi_home/translations/en.json +++ b/custom_components/xiaomi_home/translations/en.json @@ -42,12 +42,13 @@ }, "advanced_options": { "title": "Advanced Settings", - "description": "## Introduction\r\n### Unless you are very clear about the meaning of the following options, please keep the default settings.\r\n### Filter Devices\r\nSupports filtering devices by room name and device type, and also supports device dimension filtering.\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 OT protocol to achieve local control. 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### Action Debug Mode\r\nFor the methods defined by the device MIoT-Spec-V2, in addition to generating notification entities, a text input box entity will also be generated. You can use it to send control commands to the device during debugging.\r\n### Hide Non-Standard Generated Entities\r\nHide entities generated by non-standard MIoT-Spec-V2 instances with names starting with \"*\".\r\n### Display Device Status Change Notifications\r\nDisplay detailed device status change notifications, only showing the selected notifications.", + "description": "## Introduction\r\n### Unless you are very clear about the meaning of the following options, please keep the default settings.\r\n### Filter Devices\r\nSupports filtering devices by room name and device type, and also supports device dimension filtering.\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 OT protocol to achieve local control. 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### Action Debug Mode\r\nFor the methods defined by the device MIoT-Spec-V2, in addition to generating notification entities, a text input box entity will also be generated. You can use it to send control commands to the device during debugging.\r\n### Hide Non-Standard Generated Entities\r\nHide entities generated by non-standard MIoT-Spec-V2 instances with names starting with \"*\".\r\n### Binary Sensor Display Mode\r\nDisplay binary sensors in Xiaomi Home as text sensor entity or binary sensor entity。\r\n### Display Device Status Change Notifications\r\nDisplay detailed device status change notifications, only showing the selected notifications.", "data": { "devices_filter": "Filter Devices", "ctrl_mode": "Control Mode", "action_debug": "Action Debug Mode", "hide_non_standard_entities": "Hide Non-Standard Generated Entities", + "display_binary_mode": "Binary Sensor Display Mode", "display_devices_changed_notify": "Display Device Status Change Notifications" } }, @@ -119,6 +120,7 @@ "update_devices": "Update device list", "action_debug": "Debug mode for action", "hide_non_standard_entities": "Hide non-standard created entities", + "display_binary_mode": "Binary Sensor Display Mode", "display_devices_changed_notify": "Display device status change notifications", "update_trans_rules": "Update entity conversion rules", "update_lan_ctrl_config": "Update LAN control configuration", diff --git a/custom_components/xiaomi_home/translations/es.json b/custom_components/xiaomi_home/translations/es.json index e7b0c757..eb52c749 100644 --- a/custom_components/xiaomi_home/translations/es.json +++ b/custom_components/xiaomi_home/translations/es.json @@ -42,12 +42,13 @@ }, "advanced_options": { "title": "Opciones Avanzadas", - "description": "## Introducción\r\n### A menos que entienda claramente el significado de las siguientes opciones, manténgalas en su configuración predeterminada.\r\n### Filtrar dispositivos\r\nAdmite la filtración de dispositivos por nombre de habitación y tipo de dispositivo, y también admite la filtración por familia.\r\n### Modo de Control\r\n- Automático: Cuando hay una puerta de enlace central de Xiaomi disponible en la red local, Home Assistant enviará comandos de control de dispositivos a través de la puerta de enlace central para lograr la función de control local. Cuando no hay una puerta de enlace central en la red local, intentará enviar comandos de control a través del protocolo OT de Xiaomi para lograr la función de control local. Solo cuando no se cumplan las condiciones de control local anteriores, los comandos de control de dispositivos 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### Modo de Depuración de Acciones\r\nPara los métodos definidos por el dispositivo MIoT-Spec-V2, además de generar una entidad de notificación, también se generará una entidad de cuadro 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 entidades generadas por instancias MIoT-Spec-V2 no estándar que comienzan con \"*\".\r\n### Mostrar notificaciones de cambio de estado del dispositivo\r\nMostrar notificaciones detalladas de cambio de estado del dispositivo, mostrando solo las notificaciones seleccionadas.", + "description": "## Introducción\r\n### A menos que entienda claramente el significado de las siguientes opciones, manténgalas en su configuración predeterminada.\r\n### Filtrar dispositivos\r\nAdmite la filtración de dispositivos por nombre de habitación y tipo de dispositivo, y también admite la filtración por familia.\r\n### Modo de Control\r\n- Automático: Cuando hay una puerta de enlace central de Xiaomi disponible en la red local, Home Assistant enviará comandos de control de dispositivos a través de la puerta de enlace central para lograr la función de control local. Cuando no hay una puerta de enlace central en la red local, intentará enviar comandos de control a través del protocolo OT de Xiaomi para lograr la función de control local. Solo cuando no se cumplan las condiciones de control local anteriores, los comandos de control de dispositivos 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### Modo de Depuración de Acciones\r\nPara los métodos definidos por el dispositivo MIoT-Spec-V2, además de generar una entidad de notificación, también se generará una entidad de cuadro 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 entidades generadas por instancias MIoT-Spec-V2 no estándar que comienzan con \"*\".\r\n### Modo de visualización del sensor binario\r\nMuestra los sensores binarios en Xiaomi Home como entidad de sensor de texto o entidad de sensor binario。\r\n### Mostrar notificaciones de cambio de estado del dispositivo\r\nMostrar notificaciones detalladas de cambio de estado del dispositivo, mostrando solo las notificaciones seleccionadas.", "data": { "devices_filter": "Filtrar Dispositivos", "ctrl_mode": "Modo de Control", "action_debug": "Modo de Depuración de Acciones", "hide_non_standard_entities": "Ocultar Entidades Generadas No Estándar", + "display_binary_mode": "Modo de visualización del sensor binario", "display_devices_changed_notify": "Mostrar notificaciones de cambio de estado del dispositivo" } }, @@ -119,6 +120,7 @@ "update_devices": "Actualizar lista de dispositivos", "action_debug": "Modo de depuración de Action", "hide_non_standard_entities": "Ocultar entidades generadas no estándar", + "display_binary_mode": "Modo de visualización del sensor binario", "display_devices_changed_notify": "Mostrar notificaciones de cambio de estado del dispositivo", "update_trans_rules": "Actualizar reglas de conversión de entidad", "update_lan_ctrl_config": "Actualizar configuración de control LAN", diff --git a/custom_components/xiaomi_home/translations/fr.json b/custom_components/xiaomi_home/translations/fr.json index 63b9c443..07c22456 100644 --- a/custom_components/xiaomi_home/translations/fr.json +++ b/custom_components/xiaomi_home/translations/fr.json @@ -42,12 +42,13 @@ }, "advanced_options": { "title": "Paramètres Avancés", - "description": "## Introduction\r\n### Sauf si vous comprenez très bien la signification des options suivantes, veuillez les laisser par défaut.\r\n### Filtrer les appareils\r\nPrend en charge le filtrage des appareils en fonction du nom de la pièce et du type d'appareil, ainsi que le filtrage basé sur les appareils.\r\n### Mode de Contrôle\r\n- Automatique : Lorsqu'une passerelle Xiaomi est disponible dans le réseau local, Home Assistant enverra les commandes de contrôle des appareils via la passerelle pour permettre le contrôle local. Si aucune passerelle n'est disponible dans le réseau local, Home Assistant essaiera d'envoyer les commandes de contrôle des appareils via le protocole OT Xiaomi pour permettre le contrôle local. Seules si les conditions de contrôle local ci-dessus ne sont pas remplies, les commandes de contrôle des appareils seront envoyées via le cloud.\r\n- Cloud : Les commandes de contrôle des appareils sont envoyées uniquement via le cloud.\r\n### Mode de Débogage d’Actions\r\nPour les méthodes définies par les appareils MIoT-Spec-V2, en plus de générer une entité de notification, une entité de champ de texte sera également générée pour vous permettre d'envoyer des commandes de contrôle aux appareils lors du débogage.\r\n### Masquer les Entités Non Standard\r\nMasquer les entités générées par des instances MIoT-Spec-V2 non standard et commençant par \"*\".\r\n### Afficher les notifications de changement d'état de l'appareil\r\nAfficher les notifications détaillées de changement d'état de l'appareil, en affichant uniquement les notifications sélectionnées.", + "description": "## Introduction\r\n### Sauf si vous comprenez très bien la signification des options suivantes, veuillez les laisser par défaut.\r\n### Filtrer les appareils\r\nPrend en charge le filtrage des appareils en fonction du nom de la pièce et du type d'appareil, ainsi que le filtrage basé sur les appareils.\r\n### Mode de Contrôle\r\n- Automatique : Lorsqu'une passerelle Xiaomi est disponible dans le réseau local, Home Assistant enverra les commandes de contrôle des appareils via la passerelle pour permettre le contrôle local. Si aucune passerelle n'est disponible dans le réseau local, Home Assistant essaiera d'envoyer les commandes de contrôle des appareils via le protocole OT Xiaomi pour permettre le contrôle local. Seules si les conditions de contrôle local ci-dessus ne sont pas remplies, les commandes de contrôle des appareils seront envoyées via le cloud.\r\n- Cloud : Les commandes de contrôle des appareils sont envoyées uniquement via le cloud.\r\n### Mode de Débogage d’Actions\r\nPour les méthodes définies par les appareils MIoT-Spec-V2, en plus de générer une entité de notification, une entité de champ de texte sera également générée pour vous permettre d'envoyer des commandes de contrôle aux appareils lors du débogage.\r\n### Masquer les Entités Non Standard\r\nMasquer les entités générées par des instances MIoT-Spec-V2 non standard et commençant par \"*\".\r\n### Mode d'affichage du capteur binaire\r\nAffiche les capteurs binaires dans Xiaomi Home comme entité de capteur de texte ou entité de capteur binaire。\r\n### Afficher les notifications de changement d'état de l'appareil\r\nAfficher les notifications détaillées de changement d'état de l'appareil, en affichant uniquement les notifications sélectionnées.", "data": { "devices_filter": "Filtrer les Appareils", "ctrl_mode": "Mode de Contrôle", "action_debug": "Mode de Débogage d’Actions", "hide_non_standard_entities": "Masquer les Entités Non Standard", + "display_binary_mode": "Mode d'affichage du capteur binaire", "display_devices_changed_notify": "Afficher les notifications de changement d'état de l'appareil" } }, @@ -119,6 +120,7 @@ "update_devices": "Mettre à jour la liste des appareils", "action_debug": "Mode de débogage d'action", "hide_non_standard_entities": "Masquer les entités générées non standard", + "display_binary_mode": "Mode d'affichage du capteur binaire", "display_devices_changed_notify": "Afficher les notifications de changement d'état de l'appareil", "update_trans_rules": "Mettre à jour les règles de conversion d'entités", "update_lan_ctrl_config": "Mettre à jour la configuration de contrôle LAN", diff --git a/custom_components/xiaomi_home/translations/it.json b/custom_components/xiaomi_home/translations/it.json index 066ec124..b384103f 100644 --- a/custom_components/xiaomi_home/translations/it.json +++ b/custom_components/xiaomi_home/translations/it.json @@ -16,7 +16,7 @@ "cloud_server": "Regione di Login", "integration_language": "Lingua", "oauth_redirect_url": "URL di reindirizzamento OAuth2", - "network_detect_config": "Configurazione di rete integrata" + "network_detect_config": "Configurazione di rete integrata" } }, "network_detect_config": { @@ -42,12 +42,13 @@ }, "advanced_options": { "title": "Impostazioni Avanzate", - "description": "## Introduzione\r\n### A meno che tu non abbia chiaro il significato delle seguenti opzioni, si prega di mantenere le impostazioni predefinite.\r\n### Filtra Dispositivi\r\nSupporta il filtraggio dei dispositivi per nome della stanza e tipo di dispositivo, e supporta anche il filtraggio delle dimensioni del dispositivo.\r\n### Modalità di Controllo\r\n- Automatico: Quando è disponibile un gateway hub centrale Xiaomi nella rete locale, Home Assistant darà priorità all'invio dei comandi di controllo dei dispositivi tramite il gateway hub centrale per ottenere il controllo locale. Se non è presente un gateway hub centrale nella rete locale, tenterà di inviare comandi di controllo tramite il protocollo OT di Xiaomi per ottenere il controllo locale. Solo quando le condizioni di controllo locale sopra indicate non sono soddisfatte, i comandi di controllo del dispositivo verranno inviati tramite il cloud.\r\n- Cloud: Tutti i comandi di controllo vengono inviati tramite il cloud.\r\n### Modalità di Debug delle Azioni\r\nPer i metodi definiti dal dispositivo MIoT-Spec-V2, oltre a generare entità di notifica, verrà generata anche un'entità di casella di input di testo. È possibile utilizzarla per inviare comandi di controllo al dispositivo durante il debug.\r\n### Nascondi Entità Generate Non Standard\r\nNasconde le entità generate da istanze non standard MIoT-Spec-V2 con nomi che iniziano con \"*\".\r\n### Mostra Notifiche di Cambio di Stato del Dispositivo\r\nMostra notifiche dettagliate sui cambiamenti di stato del dispositivo, mostrando solo le notifiche selezionate.", + "description": "## Introduzione\r\n### A meno che tu non abbia chiaro il significato delle seguenti opzioni, si prega di mantenere le impostazioni predefinite.\r\n### Filtra Dispositivi\r\nSupporta il filtraggio dei dispositivi per nome della stanza e tipo di dispositivo, e supporta anche il filtraggio delle dimensioni del dispositivo.\r\n### Modalità di Controllo\r\n- Automatico: Quando è disponibile un gateway hub centrale Xiaomi nella rete locale, Home Assistant darà priorità all'invio dei comandi di controllo dei dispositivi tramite il gateway hub centrale per ottenere il controllo locale. Se non è presente un gateway hub centrale nella rete locale, tenterà di inviare comandi di controllo tramite il protocollo OT di Xiaomi per ottenere il controllo locale. Solo quando le condizioni di controllo locale sopra indicate non sono soddisfatte, i comandi di controllo del dispositivo verranno inviati tramite il cloud.\r\n- Cloud: Tutti i comandi di controllo vengono inviati tramite il cloud.\r\n### Modalità di Debug delle Azioni\r\nPer i metodi definiti dal dispositivo MIoT-Spec-V2, oltre a generare entità di notifica, verrà generata anche un'entità di casella di input di testo. È possibile utilizzarla per inviare comandi di controllo al dispositivo durante il debug.\r\n### Nascondi Entità Generate Non Standard\r\nNasconde le entità generate da istanze non standard MIoT-Spec-V2 con nomi che iniziano con \"*\".\r\n### Modalità di visualizzazione del sensore binario\r\nVisualizza i sensori binari in Mi Home come entità del sensore di testo o entità del sensore binario。\r\n### Mostra Notifiche di Cambio di Stato del Dispositivo\r\nMostra notifiche dettagliate sui cambiamenti di stato del dispositivo, mostrando solo le notifiche selezionate.", "data": { "devices_filter": "Filtra Dispositivi", "ctrl_mode": "Modalità di Controllo", "action_debug": "Modalità di Debug delle Azioni", "hide_non_standard_entities": "Nascondi Entità Generate Non Standard", + "display_binary_mode": "Modalità di visualizzazione del sensore binario", "display_devices_changed_notify": "Mostra Notifiche di Cambio di Stato del Dispositivo" } }, @@ -119,6 +120,7 @@ "update_devices": "Aggiorna l'elenco dei dispositivi", "action_debug": "Modalità debug per azione", "hide_non_standard_entities": "Nascondi entità create non standard", + "display_binary_mode": "Modalità di visualizzazione del sensore binario", "display_devices_changed_notify": "Mostra notifiche di cambio stato del dispositivo", "update_trans_rules": "Aggiorna le regole di conversione delle entità", "update_lan_ctrl_config": "Aggiorna configurazione del controllo LAN", @@ -219,4 +221,4 @@ "inconsistent_account": "Le informazioni dell'account sono incoerenti." } } -} +} \ No newline at end of file diff --git a/custom_components/xiaomi_home/translations/ja.json b/custom_components/xiaomi_home/translations/ja.json index 2b07b06f..2dda890b 100644 --- a/custom_components/xiaomi_home/translations/ja.json +++ b/custom_components/xiaomi_home/translations/ja.json @@ -42,12 +42,13 @@ }, "advanced_options": { "title": "高度な設定オプション", - "description": "## 紹介\r\n### 以下のオプションの意味がよくわからない場合は、デフォルトのままにしてください。\r\n### デバイスのフィルタリング\r\n部屋名とデバイスタイプでデバイスをフィルタリングすることができます。デバイスの次元でフィルタリングすることもできます。\r\n### コントロールモード\r\n- 自動:ローカルネットワーク内に利用可能なXiaomi中央ゲートウェイがある場合、Home Assistantはデバイス制御命令を送信するために優先的に中央ゲートウェイを使用します。ローカルネットワークに中央ゲートウェイがない場合、Xiaomi OTプロトコルを使用してデバイス制御命令を送信し、ローカル制御機能を実現します。上記のローカル制御条件が満たされない場合のみ、デバイス制御命令はクラウドを介して送信されます。\r\n- クラウド:制御命令はクラウドを介してのみ送信されます。\r\n### Actionデバッグモード\r\nデバイスが定義するMIoT-Spec-V2のメソッドに対して、通知エンティティを生成するだけでなく、デバイスに制御命令を送信するためのテキスト入力ボックスエンティティも生成されます。デバッグ時にデバイスに制御命令を送信するために使用できます。\r\n### 非標準生成エンティティを隠す\r\n「*」で始まる名前の非標準MIoT-Spec-V2インスタンスによって生成されたエンティティを非表示にします。\r\n### デバイスの状態変化通知を表示\r\nデバイスの状態変化通知を詳細に表示し、選択された通知のみを表示します。", + "description": "## 紹介\r\n### 以下のオプションの意味がよくわからない場合は、デフォルトのままにしてください。\r\n### デバイスのフィルタリング\r\n部屋名とデバイスタイプでデバイスをフィルタリングすることができます。デバイスの次元でフィルタリングすることもできます。\r\n### コントロールモード\r\n- 自動:ローカルネットワーク内に利用可能なXiaomi中央ゲートウェイがある場合、Home Assistantはデバイス制御命令を送信するために優先的に中央ゲートウェイを使用します。ローカルネットワークに中央ゲートウェイがない場合、Xiaomi OTプロトコルを使用してデバイス制御命令を送信し、ローカル制御機能を実現します。上記のローカル制御条件が満たされない場合のみ、デバイス制御命令はクラウドを介して送信されます。\r\n- クラウド:制御命令はクラウドを介してのみ送信されます。\r\n### Actionデバッグモード\r\nデバイスが定義するMIoT-Spec-V2のメソッドに対して、通知エンティティを生成するだけでなく、デバイスに制御命令を送信するためのテキスト入力ボックスエンティティも生成されます。デバッグ時にデバイスに制御命令を送信するために使用できます。\r\n### 非標準生成エンティティを隠す\r\n「*」で始まる名前の非標準MIoT-Spec-V2インスタンスによって生成されたエンティティを非表示にします。\r\n### バイナリセンサー表示モード\r\nXiaomi Homeのバイナリセンサーをテキストセンサーエンティティまたはバイナリセンサーエンティティとして表示します。\r\n### デバイスの状態変化通知を表示\r\nデバイスの状態変化通知を詳細に表示し、選択された通知のみを表示します。", "data": { "devices_filter": "デバイスをフィルタリング", "ctrl_mode": "コントロールモード", "action_debug": "Actionデバッグモード", "hide_non_standard_entities": "非標準生成エンティティを隠す", + "display_binary_mode": "バイナリセンサー表示モード", "display_devices_changed_notify": "デバイスの状態変化通知を表示" } }, @@ -119,6 +120,7 @@ "update_devices": "デバイスリストを更新する", "action_debug": "Action デバッグモード", "hide_non_standard_entities": "非標準生成エンティティを非表示にする", + "display_binary_mode": "バイナリセンサー表示モード", "display_devices_changed_notify": "デバイスの状態変化通知を表示", "update_trans_rules": "エンティティ変換ルールを更新する", "update_lan_ctrl_config": "LAN制御構成を更新する", diff --git a/custom_components/xiaomi_home/translations/nl.json b/custom_components/xiaomi_home/translations/nl.json index 6e289369..c9d8e400 100644 --- a/custom_components/xiaomi_home/translations/nl.json +++ b/custom_components/xiaomi_home/translations/nl.json @@ -42,12 +42,13 @@ }, "advanced_options": { "title": "Geavanceerde Instellingen", - "description": "## Inleiding\r\n### Tenzij u zeer goed op de hoogte bent van de betekenis van de volgende opties, houdt u de standaardinstellingen.\r\n### Apparaten filteren\r\nOndersteunt het filteren van apparaten op basis van kamer- en apparaattypen, en ondersteunt ook apparaatdimensiefiltering.\r\n### Besturingsmodus\r\n- Automatisch: Wanneer er een beschikbare Xiaomi centrale hubgateway in het lokale netwerk is, zal Home Assistant eerst apparaatbesturingsinstructies via de centrale hubgateway verzenden om lokale controlefunctionaliteit te bereiken. Als er geen centrale hub in het lokale netwerk is, zal het proberen om besturingsinstructies via het Xiaomi OT-protocol te verzenden om lokale controlefunctionaliteit te bereiken. Alleen als de bovenstaande lokale controlevoorwaarden niet worden vervuld, worden apparaatbesturingsinstructies via de cloud verzonden.\r\n- Cloud: Besturingsinstructies worden alleen via de cloud verzonden.\r\n### Actie-debugmodus\r\nVoor methoden die zijn gedefinieerd in de MIoT-Spec-V2 van het apparaat, wordt naast het genereren van een meldingsentiteit ook een tekstinvoerveldentiteit gegenereerd. U kunt dit gebruiken om besturingsinstructies naar het apparaat te sturen tijdens het debuggen.\r\n### Niet-standaard entiteiten verbergen\r\nVerberg entiteiten die zijn gegenereerd door niet-standaard MIoT-Spec-V2-instanties die beginnen met \"*\".\r\n### Apparaatstatuswijzigingen weergeven\r\nGedetailleerde apparaatstatuswijzigingen weergeven, alleen de geselecteerde meldingen weergeven.", + "description": "## Inleiding\r\n### Tenzij u zeer goed op de hoogte bent van de betekenis van de volgende opties, houdt u de standaardinstellingen.\r\n### Apparaten filteren\r\nOndersteunt het filteren van apparaten op basis van kamer- en apparaattypen, en ondersteunt ook apparaatdimensiefiltering.\r\n### Besturingsmodus\r\n- Automatisch: Wanneer er een beschikbare Xiaomi centrale hubgateway in het lokale netwerk is, zal Home Assistant eerst apparaatbesturingsinstructies via de centrale hubgateway verzenden om lokale controlefunctionaliteit te bereiken. Als er geen centrale hub in het lokale netwerk is, zal het proberen om besturingsinstructies via het Xiaomi OT-protocol te verzenden om lokale controlefunctionaliteit te bereiken. Alleen als de bovenstaande lokale controlevoorwaarden niet worden vervuld, worden apparaatbesturingsinstructies via de cloud verzonden.\r\n- Cloud: Besturingsinstructies worden alleen via de cloud verzonden.\r\n### Actie-debugmodus\r\nVoor methoden die zijn gedefinieerd in de MIoT-Spec-V2 van het apparaat, wordt naast het genereren van een meldingsentiteit ook een tekstinvoerveldentiteit gegenereerd. U kunt dit gebruiken om besturingsinstructies naar het apparaat te sturen tijdens het debuggen.\r\n### Niet-standaard entiteiten verbergen\r\nVerberg entiteiten die zijn gegenereerd door niet-standaard MIoT-Spec-V2-instanties die beginnen met \"*\".\r\n### Binaire sensorweergavemodus\r\nToont binaire sensoren in Xiaomi Home als tekstsensor-entiteit of binairesensor-entiteit。\r\n### Apparaatstatuswijzigingen weergeven\r\nGedetailleerde apparaatstatuswijzigingen weergeven, alleen de geselecteerde meldingen weergeven.", "data": { "devices_filter": "Apparaten filteren", "ctrl_mode": "Besturingsmodus", "action_debug": "Actie-debugmodus", "hide_non_standard_entities": "Niet-standaard entiteiten verbergen", + "display_binary_mode": "Binaire sensorweergavemodus", "display_devices_changed_notify": "Apparaatstatuswijzigingen weergeven" } }, @@ -119,6 +120,7 @@ "update_devices": "Werk apparatenlijst bij", "action_debug": "Debugmodus voor actie", "hide_non_standard_entities": "Verberg niet-standaard gemaakte entiteiten", + "display_binary_mode": "Binaire sensorweergavemodus", "display_devices_changed_notify": "Apparaatstatuswijzigingen weergeven", "update_trans_rules": "Werk entiteitsconversieregels bij", "update_lan_ctrl_config": "Werk LAN controleconfiguratie bij", diff --git a/custom_components/xiaomi_home/translations/pt-BR.json b/custom_components/xiaomi_home/translations/pt-BR.json index 3adcd0d1..12286d56 100644 --- a/custom_components/xiaomi_home/translations/pt-BR.json +++ b/custom_components/xiaomi_home/translations/pt-BR.json @@ -42,12 +42,13 @@ }, "advanced_options": { "title": "Configurações Avançadas", - "description": "## Introdução\r\n### A menos que você entenda claramente o significado das opções a seguir, mantenha as configurações padrão.\r\n### Filtrar Dispositivos\r\nSuporte para filtrar dispositivos por nome da sala e tipo de dispositivo, bem como filtragem por família.\r\n### Modo de Controle\r\n- Automático: Quando um gateway central Xiaomi disponível na rede local está disponível, o Home Assistant enviará comandos de controle de dispositivo através do gateway central para realizar a função de controle local. Quando não há gateway central na rede local, ele tentará enviar comandos de controle através do protocolo OT da Xiaomi para realizar a função de controle local. Somente quando as condições de controle local acima não forem atendidas, os comandos de controle do dispositivo serão enviados através da nuvem.\r\n- Nuvem: Os comandos de controle são enviados apenas através da nuvem.\r\n### Modo de Depuração de Ações\r\nPara métodos definidos pelo MIoT-Spec-V2 do dispositivo, além de gerar uma entidade de notificação, também será gerada uma entidade de caixa de texto para você enviar comandos de controle ao dispositivo durante a depuração.\r\n### Ocultar Entidades Geradas Não Padrão\r\nOcultar entidades geradas por instâncias MIoT-Spec-V2 não padrão que começam com \"*\".\r\n### Exibir notificações de mudança de status do dispositivo\r\nExibir notificações detalhadas de mudança de status do dispositivo, mostrando apenas as notificações selecionadas.", + "description": "## Introdução\r\n### A menos que você entenda claramente o significado das opções a seguir, mantenha as configurações padrão.\r\n### Filtrar Dispositivos\r\nSuporte para filtrar dispositivos por nome da sala e tipo de dispositivo, bem como filtragem por família.\r\n### Modo de Controle\r\n- Automático: Quando um gateway central Xiaomi disponível na rede local está disponível, o Home Assistant enviará comandos de controle de dispositivo através do gateway central para realizar a função de controle local. Quando não há gateway central na rede local, ele tentará enviar comandos de controle através do protocolo OT da Xiaomi para realizar a função de controle local. Somente quando as condições de controle local acima não forem atendidas, os comandos de controle do dispositivo serão enviados através da nuvem.\r\n- Nuvem: Os comandos de controle são enviados apenas através da nuvem.\r\n### Modo de Depuração de Ações\r\nPara métodos definidos pelo MIoT-Spec-V2 do dispositivo, além de gerar uma entidade de notificação, também será gerada uma entidade de caixa de texto para você enviar comandos de controle ao dispositivo durante a depuração.\r\n### Ocultar Entidades Geradas Não Padrão\r\nOcultar entidades geradas por instâncias MIoT-Spec-V2 não padrão que começam com \"*\".\r\n### Modo de exibição do sensor binário\r\nExibe sensores binários no Xiaomi Home como entidade de sensor de texto ou entidade de sensor binário。\r\n### Exibir notificações de mudança de status do dispositivo\r\nExibir notificações detalhadas de mudança de status do dispositivo, mostrando apenas as notificações selecionadas.", "data": { "devices_filter": "Filtrar Dispositivos", "ctrl_mode": "Modo de Controle", "action_debug": "Modo de Depuração de Ações", "hide_non_standard_entities": "Ocultar Entidades Geradas Não Padrão", + "display_binary_mode": "Modo de exibição do sensor binário", "display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo" } }, @@ -119,6 +120,7 @@ "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", + "display_binary_mode": "Modo de exibição do sensor binário", "display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo", "update_trans_rules": "Atualizar regras de conversão de entidades", "update_lan_ctrl_config": "Atualizar configuração de controle LAN", diff --git a/custom_components/xiaomi_home/translations/pt.json b/custom_components/xiaomi_home/translations/pt.json index ce58cd55..22875851 100644 --- a/custom_components/xiaomi_home/translations/pt.json +++ b/custom_components/xiaomi_home/translations/pt.json @@ -42,12 +42,13 @@ }, "advanced_options": { "title": "Opções Avançadas", - "description": "## Introdução\r\n### A menos que você entenda claramente o significado das opções abaixo, mantenha as configurações padrão.\r\n### Filtrar Dispositivos\r\nSuporte para filtrar dispositivos por nome da sala e tipo de dispositivo, bem como filtragem por família.\r\n### Modo de Controle\r\n- Automático: Quando um gateway central Xiaomi está disponível na rede local, o Home Assistant enviará comandos de controlo de dispositivos através do gateway central para realizar o controlo local. Quando não há gateway central na rede local, tentará enviar comandos de controlo através do protocolo Xiaomi OT para realizar o controlo local. Apenas quando as condições de controlo local acima não são atendidas, os comandos de controlo de dispositivos serão enviados através da nuvem.\r\n- Nuvem: Os comandos de controlo são enviados apenas através da nuvem.\r\n### Modo de Depuração de Ações\r\nPara métodos definidos pelo MIoT-Spec-V2, além de gerar uma entidade de notificação, também será gerada uma entidade de caixa de texto para depuração de controlo de dispositivos.\r\n### Ocultar Entidades Geradas Não Padrão\r\nOcultar entidades geradas por instâncias MIoT-Spec-V2 não padrão, cujos nomes começam com \"*\".\r\n### Exibir notificações de mudança de status do dispositivo\r\nExibir notificações detalhadas de mudança de status do dispositivo, mostrando apenas as notificações selecionadas.", + "description": "## Introdução\r\n### A menos que você entenda claramente o significado das opções abaixo, mantenha as configurações padrão.\r\n### Filtrar Dispositivos\r\nSuporte para filtrar dispositivos por nome da sala e tipo de dispositivo, bem como filtragem por família.\r\n### Modo de Controle\r\n- Automático: Quando um gateway central Xiaomi está disponível na rede local, o Home Assistant enviará comandos de controlo de dispositivos através do gateway central para realizar o controlo local. Quando não há gateway central na rede local, tentará enviar comandos de controlo através do protocolo Xiaomi OT para realizar o controlo local. Apenas quando as condições de controlo local acima não são atendidas, os comandos de controlo de dispositivos serão enviados através da nuvem.\r\n- Nuvem: Os comandos de controlo são enviados apenas através da nuvem.\r\n### Modo de Depuração de Ações\r\nPara métodos definidos pelo MIoT-Spec-V2, além de gerar uma entidade de notificação, também será gerada uma entidade de caixa de texto para depuração de controlo de dispositivos.\r\n### Ocultar Entidades Geradas Não Padrão\r\nOcultar entidades geradas por instâncias MIoT-Spec-V2 não padrão, cujos nomes começam com \"*\".\r\n### Modo de exibição do sensor binário\r\nExibe sensores binários no Xiaomi Home como entidade de sensor de texto ou entidade de sensor binário。\r\n### Exibir notificações de mudança de status do dispositivo\r\nExibir notificações detalhadas de mudança de status do dispositivo, mostrando apenas as notificações selecionadas.", "data": { "devices_filter": "Filtrar Dispositivos", "ctrl_mode": "Modo de Controlo", "action_debug": "Modo de Depuração de Ações", "hide_non_standard_entities": "Ocultar Entidades Geradas Não Padrão", + "display_binary_mode": "Modo de exibição do sensor binário", "display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo" } }, @@ -119,6 +120,7 @@ "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", + "display_binary_mode": "Modo de exibição do sensor binário", "display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo", "update_trans_rules": "Atualizar regras de conversão de entidades", "update_lan_ctrl_config": "Atualizar configuração de controlo LAN", diff --git a/custom_components/xiaomi_home/translations/ru.json b/custom_components/xiaomi_home/translations/ru.json index a4928697..fba3edc5 100644 --- a/custom_components/xiaomi_home/translations/ru.json +++ b/custom_components/xiaomi_home/translations/ru.json @@ -42,12 +42,13 @@ }, "advanced_options": { "title": "Расширенные настройки", - "description": "## Введение\r\n### Если вы не очень хорошо понимаете значение следующих параметров, оставьте их по умолчанию.\r\n### Фильтрация устройств\r\nПоддерживает фильтрацию устройств по названию комнаты и типу устройства, а также фильтрацию по уровню устройства.\r\n### Режим управления\r\n- Автоматически: при наличии доступного центрального шлюза Xiaomi в локальной сети Home Assistant Home Assistant будет отправлять команды управления устройствами через центральный шлюз для локального управления. Если центрального шлюза нет в локальной сети, Home Assistant попытается отправить команды управления устройствами через протокол OT Xiaomi для локального управления. Только если вышеуказанные условия локального управления не выполняются, команды управления устройствами будут отправляться через облако.\r\n- Облако: команды управления отправляются только через облако.\r\n### Режим отладки действий\r\nДля методов, определенных устройством MIoT-Spec-V2, помимо создания уведомления, будет создана сущность текстового поля, которую вы можете использовать для отправки команд управления устройством во время отладки.\r\n### Скрыть нестандартные сущности\r\nСкрыть сущности, созданные нестандартными экземплярами MIoT-Spec-V2, имена которых начинаются с «*».\r\n### Отображать уведомления о изменении состояния устройства\r\nОтображать подробные уведомления о изменении состояния устройства, показывая только выбранные уведомления.", + "description": "## Введение\r\n### Если вы не очень хорошо понимаете значение следующих параметров, оставьте их по умолчанию.\r\n### Фильтрация устройств\r\nПоддерживает фильтрацию устройств по названию комнаты и типу устройства, а также фильтрацию по уровню устройства.\r\n### Режим управления\r\n- Автоматически: при наличии доступного центрального шлюза Xiaomi в локальной сети Home Assistant Home Assistant будет отправлять команды управления устройствами через центральный шлюз для локального управления. Если центрального шлюза нет в локальной сети, Home Assistant попытается отправить команды управления устройствами через протокол OT Xiaomi для локального управления. Только если вышеуказанные условия локального управления не выполняются, команды управления устройствами будут отправляться через облако.\r\n- Облако: команды управления отправляются только через облако.\r\n### Режим отладки действий\r\nДля методов, определенных устройством MIoT-Spec-V2, помимо создания уведомления, будет создана сущность текстового поля, которую вы можете использовать для отправки команд управления устройством во время отладки.\r\n### Скрыть нестандартные сущности\r\nСкрыть сущности, созданные нестандартными экземплярами MIoT-Spec-V2, имена которых начинаются с «*».\r\n### Режим отображения бинарного датчика\r\nОтображает бинарные датчики в Xiaomi Home как сущность текстового датчика или сущность бинарного датчика。\r\n### Отображать уведомления о изменении состояния устройства\r\nОтображать подробные уведомления о изменении состояния устройства, показывая только выбранные уведомления.", "data": { "devices_filter": "Фильтрация устройств", "ctrl_mode": "Режим управления", "action_debug": "Режим отладки действий", "hide_non_standard_entities": "Скрыть нестандартные сущности", + "display_binary_mode": "Режим отображения бинарного датчика", "display_devices_changed_notify": "Отображать уведомления о изменении состояния устройства" } }, @@ -119,6 +120,7 @@ "update_devices": "Обновить список устройств", "action_debug": "Режим отладки Action", "hide_non_standard_entities": "Скрыть нестандартные сущности", + "display_binary_mode": "Режим отображения бинарного датчика", "display_devices_changed_notify": "Отображать уведомления о изменении состояния устройства", "update_trans_rules": "Обновить правила преобразования сущностей", "update_lan_ctrl_config": "Обновить конфигурацию управления LAN", diff --git a/custom_components/xiaomi_home/translations/zh-Hans.json b/custom_components/xiaomi_home/translations/zh-Hans.json index 39859da9..67c134c4 100644 --- a/custom_components/xiaomi_home/translations/zh-Hans.json +++ b/custom_components/xiaomi_home/translations/zh-Hans.json @@ -42,12 +42,13 @@ }, "advanced_options": { "title": "高级设置选项", - "description": "## 使用介绍\r\n### 除非您非常清楚下列选项的含义,否则请保持默认。\r\n### 筛选设备\r\n支持按照家庭房间名称、设备接入类型、设备型号筛选设备,同时也支持设备维度筛选。\r\n### 控制模式\r\n- 自动:本地局域网内存在可用的小米中枢网关时, Home Assistant 会优先通过中枢网关发送设备控制指令,以实现本地化控制功能。本地局域网不存在中枢时,会尝试通过小米OT协议发送控制指令,以实现本地化控制功能。只有当上述本地化控制条件不满足时,设备控制指令才会通过云端发送。\r\n- 云端:控制指令仅通过云端发送。\r\n### Action 调试模式\r\n对于设备 MIoT-Spec-V2 定义的方法,在生成通知实体之外,还会生成一个文本输入框实体,您可以在调试时用它向设备发送控制指令。\r\n### 隐藏非标准生成实体\r\n隐藏名称以“*”开头的非标准 MIoT-Spec-V2 实例生成的实体。\r\n### 显示设备状态变化通知\r\n细化显示设备状态变化通知,只显示勾选的通知消息。", + "description": "## 使用介绍\r\n### 除非您非常清楚下列选项的含义,否则请保持默认。\r\n### 筛选设备\r\n支持按照家庭房间名称、设备接入类型、设备型号筛选设备,同时也支持设备维度筛选。\r\n### 控制模式\r\n- 自动:本地局域网内存在可用的小米中枢网关时, Home Assistant 会优先通过中枢网关发送设备控制指令,以实现本地化控制功能。本地局域网不存在中枢时,会尝试通过小米OT协议发送控制指令,以实现本地化控制功能。只有当上述本地化控制条件不满足时,设备控制指令才会通过云端发送。\r\n- 云端:控制指令仅通过云端发送。\r\n### Action 调试模式\r\n对于设备 MIoT-Spec-V2 定义的方法,在生成通知实体之外,还会生成一个文本输入框实体,您可以在调试时用它向设备发送控制指令。\r\n### 隐藏非标准生成实体\r\n隐藏名称以“*”开头的非标准 MIoT-Spec-V2 实例生成的实体。\r\n### 二进制传感器显示模式\r\n将米家中的二进制传感器显示为文本传感器实体或者二进制传感器实体。\r\n### 显示设备状态变化通知\r\n细化显示设备状态变化通知,只显示勾选的通知消息。", "data": { "devices_filter": "筛选设备", "ctrl_mode": "控制模式", "action_debug": "Action 调试模式", "hide_non_standard_entities": "隐藏非标准生成实体", + "display_binary_mode": "二进制传感器显示模式", "display_devices_changed_notify": "显示设备状态变化通知" } }, @@ -119,6 +120,7 @@ "update_devices": "更新设备列表", "action_debug": "Action 调试模式", "hide_non_standard_entities": "隐藏非标准生成实体", + "display_binary_mode": "二进制传感器显示模式", "display_devices_changed_notify": "显示设备状态变化通知", "update_trans_rules": "更新实体转换规则", "update_lan_ctrl_config": "更新局域网控制配置", diff --git a/custom_components/xiaomi_home/translations/zh-Hant.json b/custom_components/xiaomi_home/translations/zh-Hant.json index 59580aea..68cc982a 100644 --- a/custom_components/xiaomi_home/translations/zh-Hant.json +++ b/custom_components/xiaomi_home/translations/zh-Hant.json @@ -42,12 +42,13 @@ }, "advanced_options": { "title": "高級設置選項", - "description": "## 使用介紹\r\n### 除非您非常清楚下列選項的含義,否則請保持默認。\r\n### 篩選設備\r\n支持按照房間名稱和設備類型篩選設備,同時也支持設備維度篩選。\r\n### 控制模式\r\n- 自動:本地局域網內存在可用的小米中樞網關時, Home Assistant 會優先通過中樞網關發送設備控制指令,以實現本地化控制功能。本地局域網不存在中樞時,會嘗試通過小米OT協議發送控制指令,以實現本地化控制功能。只有當上述本地化控制條件不滿足時,設備控制指令才會通過雲端發送。\r\n- 雲端:控制指令僅通過雲端發送。\r\n### Action 調試模式\r\n對於設備 MIoT-Spec-V2 定義的方法,在生成通知實體之外,還會生成一個文本輸入框實體,您可以在調試時用它向設備發送控制指令。\r\n### 隱藏非標準生成實體\r\n隱藏名稱以“*”開頭的非標準 MIoT-Spec-V2 實例生成的實體。\r\n### 顯示設備狀態變化通知\r\n細化顯示設備狀態變化通知,只顯示勾選的通知消息。", + "description": "## 使用介紹\r\n### 除非您非常清楚下列選項的含義,否則請保持默認。\r\n### 篩選設備\r\n支持按照房間名稱和設備類型篩選設備,同時也支持設備維度篩選。\r\n### 控制模式\r\n- 自動:本地局域網內存在可用的小米中樞網關時, Home Assistant 會優先通過中樞網關發送設備控制指令,以實現本地化控制功能。本地局域網不存在中樞時,會嘗試通過小米OT協議發送控制指令,以實現本地化控制功能。只有當上述本地化控制條件不滿足時,設備控制指令才會通過雲端發送。\r\n- 雲端:控制指令僅通過雲端發送。\r\n### Action 調試模式\r\n對於設備 MIoT-Spec-V2 定義的方法,在生成通知實體之外,還會生成一個文本輸入框實體,您可以在調試時用它向設備發送控制指令。\r\n### 隱藏非標準生成實體\r\n隱藏名稱以“*”開頭的非標準 MIoT-Spec-V2 實例生成的實體。\r\n### 二進制傳感器顯示模式\r\n將米家中的二進制傳感器顯示為文本傳感器實體或者二進制傳感器實體。\r\n### 顯示設備狀態變化通知\r\n細化顯示設備狀態變化通知,只顯示勾選的通知消息。", "data": { "devices_filter": "篩選設備", "ctrl_mode": "控制模式", "action_debug": "Action 調試模式", "hide_non_standard_entities": "隱藏非標準生成實體", + "display_binary_mode": "二進制傳感器顯示模式", "display_devices_changed_notify": "顯示設備狀態變化通知" } }, @@ -119,6 +120,7 @@ "update_devices": "更新設備列表", "action_debug": "Action 調試模式", "hide_non_standard_entities": "隱藏非標準生成實體", + "display_binary_mode": "二進制傳感器顯示模式", "display_devices_changed_notify": "顯示設備狀態變化通知", "update_trans_rules": "更新實體轉換規則", "update_lan_ctrl_config": "更新局域網控制配置", diff --git a/custom_components/xiaomi_home/vacuum.py b/custom_components/xiaomi_home/vacuum.py index fda2d5a4..232e6769 100644 --- a/custom_components/xiaomi_home/vacuum.py +++ b/custom_components/xiaomi_home/vacuum.py @@ -120,28 +120,18 @@ def __init__( # properties for prop in entity_data.props: if prop.name == 'status': - if ( - not isinstance(prop.value_list, list) - or not prop.value_list - ): + if not prop.value_list: _LOGGER.error( 'invalid status value_list, %s', self.entity_id) continue - self._status_map = { - item['value']: item['description'] - for item in prop.value_list} + self._status_map = prop.value_list.to_map() self._prop_status = prop elif prop.name == 'fan-level': - if ( - not isinstance(prop.value_list, list) - or not prop.value_list - ): + if not prop.value_list: _LOGGER.error( 'invalid fan-level value_list, %s', self.entity_id) continue - self._fan_level_map = { - item['value']: item['description'] - for item in prop.value_list} + self._fan_level_map = prop.value_list.to_map() self._attr_fan_speed_list = list(self._fan_level_map.values()) self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED self._prop_fan_level = prop @@ -202,7 +192,7 @@ async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: @property def state(self) -> Optional[str]: """Return the current state of the vacuum cleaner.""" - return self.get_map_description( + return self.get_map_value( map_=self._status_map, key=self.get_prop_value(prop=self._prop_status)) @@ -214,6 +204,6 @@ def battery_level(self) -> Optional[int]: @property def fan_speed(self) -> Optional[str]: """Return the current fan speed of the vacuum cleaner.""" - return self.get_map_description( + return self.get_map_value( map_=self._fan_level_map, key=self.get_prop_value(prop=self._prop_fan_level)) diff --git a/custom_components/xiaomi_home/water_heater.py b/custom_components/xiaomi_home/water_heater.py index aa7fe67b..aba60934 100644 --- a/custom_components/xiaomi_home/water_heater.py +++ b/custom_components/xiaomi_home/water_heater.py @@ -93,7 +93,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity): _prop_target_temp: Optional[MIoTSpecProperty] _prop_mode: Optional[MIoTSpecProperty] - _mode_list: Optional[dict[Any, Any]] + _mode_map: Optional[dict[Any, Any]] def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData @@ -106,7 +106,7 @@ def __init__( self._prop_temp = None self._prop_target_temp = None self._prop_mode = None - self._mode_list = None + self._mode_map = None # properties for prop in entity_data.props: @@ -115,7 +115,7 @@ def __init__( self._prop_on = prop # temperature if prop.name == 'temperature': - if isinstance(prop.value_range, dict): + if prop.value_range: if ( self._attr_temperature_unit is None and prop.external_unit @@ -128,9 +128,14 @@ 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 not prop.value_range: + _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_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 |= ( @@ -138,17 +143,12 @@ def __init__( self._prop_target_temp = prop # mode if prop.name == 'mode': - if ( - not isinstance(prop.value_list, list) - or not prop.value_list - ): + if not prop.value_list: _LOGGER.error( 'mode value_list is None, %s', self.entity_id) continue - self._mode_list = { - item['value']: item['description'] - for item in prop.value_list} - self._attr_operation_list = list(self._mode_list.values()) + self._mode_map = prop.value_list.to_map() + self._attr_operation_list = list(self._mode_map.values()) self._attr_supported_features |= ( WaterHeaterEntityFeature.OPERATION_MODE) self._prop_mode = prop @@ -184,7 +184,9 @@ async def async_set_operation_mode(self, operation_mode: str) -> None: 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)) + value=self.get_map_key( + map_=self._mode_map, + value=operation_mode)) async def async_turn_away_mode_on(self) -> None: """Set the water heater to away mode.""" @@ -207,20 +209,6 @@ def current_operation(self) -> Optional[str]: 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( + return self.get_map_value( + map_=self._mode_map, key=self.get_prop_value(prop=self._prop_mode)) - - def __get_mode_description(self, key: int) -> Optional[str]: - """Convert mode value to description.""" - if self._mode_list is None: - return None - return self._mode_list.get(key, None) - - def __get_mode_value(self, description: str) -> Optional[int]: - """Convert mode description to value.""" - if self._mode_list is None: - return None - for key, value in self._mode_list.items(): - if value == description: - return key - return None diff --git a/test/check_rule_format.py b/test/check_rule_format.py index 5075367a..18ce0340 100644 --- a/test/check_rule_format.py +++ b/test/check_rule_format.py @@ -16,13 +16,10 @@ 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') + '../custom_components/xiaomi_home/miot/specs/bool_trans.yaml') SPEC_FILTER_FILE = path.join( ROOT_PATH, - '../custom_components/xiaomi_home/miot/specs/spec_filter.json') + '../custom_components/xiaomi_home/miot/specs/spec_filter.yaml') def load_json_file(file_path: str) -> Optional[dict]: @@ -54,6 +51,12 @@ def load_yaml_file(file_path: str) -> Optional[dict]: return None +def save_yaml_file(file_path: str, data: dict) -> None: + with open(file_path, 'w', encoding='utf-8') as file: + yaml.safe_dump( + data, file, default_flow_style=False, allow_unicode=True, indent=2) + + def dict_str_str(d: dict) -> bool: """restricted format: dict[str, str]""" if not isinstance(d, dict): @@ -161,25 +164,17 @@ def compare_dict_structure(dict1: dict, dict2: dict) -> bool: def sort_bool_trans(file_path: str): - trans_data: dict = load_json_file(file_path=file_path) + trans_data = load_yaml_file(file_path=file_path) + assert isinstance(trans_data, dict), f'{file_path} format error' 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 = load_yaml_file(file_path=file_path) + assert isinstance(filter_data, dict), f'{file_path} format error' filter_data = dict(sorted(filter_data.items())) for urn, spec in filter_data.items(): filter_data[urn] = dict(sorted(spec.items())) @@ -188,30 +183,26 @@ def sort_spec_filter(file_path: str): @pytest.mark.github def test_bool_trans(): - data: dict = load_json_file(SPEC_BOOL_TRANS_FILE) + data = load_yaml_file(SPEC_BOOL_TRANS_FILE) + assert isinstance(data, dict) 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(SPEC_FILTER_FILE) + data = load_yaml_file(SPEC_FILTER_FILE) + assert isinstance(data, dict) 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(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(): 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) + data = load_json_file(file_path) + assert isinstance(data, dict) assert data, f'load {file_path} failed' assert nested_3_dict_str_str(data), f'{file_path} format error' @@ -220,7 +211,8 @@ def test_miot_i18n(): def test_translations(): 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) + data = load_json_file(file_path) + assert isinstance(data, dict) assert data, f'load {file_path} failed' assert dict_str_dict(data), f'{file_path} format error' @@ -237,15 +229,16 @@ def test_miot_lang_integrity(): 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_data = load_yaml_file(SPEC_BOOL_TRANS_FILE) + assert isinstance(bool_trans_data, dict) 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( + default_dict = load_json_file( path.join(TRANS_RELATIVE_PATH, integration_lang_list[0])) for name in list(integration_lang_list)[1:]: - compare_dict: dict = load_json_file( + compare_dict = load_json_file( path.join(TRANS_RELATIVE_PATH, name)) if not compare_dict_structure(default_dict, compare_dict): _LOGGER.info( @@ -255,7 +248,7 @@ def test_miot_lang_integrity(): default_dict = load_json_file( path.join(MIOT_I18N_RELATIVE_PATH, integration_lang_list[0])) for name in list(integration_lang_list)[1:]: - compare_dict: dict = load_json_file( + compare_dict = load_json_file( path.join(MIOT_I18N_RELATIVE_PATH, name)) if not compare_dict_structure(default_dict, compare_dict): _LOGGER.info( @@ -272,19 +265,13 @@ def test_miot_data_sort(): '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( + load_yaml_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( + load_yaml_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, ', @@ -294,11 +281,8 @@ def test_miot_data_sort(): @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) + save_yaml_file(file_path=SPEC_BOOL_TRANS_FILE, data=sort_data) _LOGGER.info('%s formatted.', SPEC_BOOL_TRANS_FILE) - sort_data = sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE) - save_json_file(file_path=SPEC_MULTI_LANG_FILE, data=sort_data) - _LOGGER.info('%s formatted.', SPEC_MULTI_LANG_FILE) sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE) - save_json_file(file_path=SPEC_FILTER_FILE, data=sort_data) + save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data) _LOGGER.info('%s formatted.', SPEC_FILTER_FILE)