diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index 42897d7..5a390b2 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -189,7 +189,7 @@ class FeaturePresetMode(MIoTServiceEntity, ClimateEntity): for prop in self.entity_data.props: if prop.name == prop_name and prop.service.name == service_name: if not prop.value_list: - _LOGGER.error('invalid %s %s value_list, %s',service_name, + _LOGGER.error('invalid %s %s value_list, %s', service_name, prop_name, self.entity_id) continue self._mode_map = prop.value_list.to_map() @@ -229,7 +229,9 @@ class FeatureFanMode(MIoTServiceEntity, ClimateEntity): super().__init__(miot_device=miot_device, entity_data=entity_data) # properties for prop in entity_data.props: - if prop.name == 'fan-level' and prop.service.name == 'fan-control': + if (prop.name == 'fan-level' and + (prop.service.name == 'fan-control' or + prop.service.name == 'thermostat')): if not prop.value_list: _LOGGER.error('invalid fan-level value_list, %s', self.entity_id) @@ -665,78 +667,31 @@ class PtcBathHeater(FeatureTargetTemperature, FeatureTemperature, class Thermostat(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature, - FeatureHumidity, FeatureFanMode): + FeatureHumidity, FeatureFanMode, FeaturePresetMode): """Thermostat""" - _prop_mode: Optional[MIoTSpecProperty] - _hvac_mode_map: Optional[dict[int, HVACMode]] def __init__(self, miot_device: MIoTDevice, entity_data: MIoTEntityData) -> None: """Initialize the thermostat.""" - self._prop_mode = None - self._hvac_mode_map = None - super().__init__(miot_device=miot_device, entity_data=entity_data) + self._attr_icon = 'mdi:thermostat' # hvac modes - self._attr_hvac_modes = None - for prop in entity_data.props: - if prop.name == 'mode': - 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.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 - - if self._attr_hvac_modes is None: - self._attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO] - elif HVACMode.OFF not in self._attr_hvac_modes: - self._attr_hvac_modes.insert(0, HVACMode.OFF) + self._attr_hvac_modes = [HVACMode.AUTO, HVACMode.OFF] + # preset modes + self._init_preset_modes('thermostat', 'mode') async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the target hvac mode.""" - # set the device off - if hvac_mode == HVACMode.OFF: - if not await self.set_property_async(prop=self._prop_on, - value=False): - raise RuntimeError(f'set climate prop.on failed, {hvac_mode}, ' - f'{self.entity_id}') - return - # set the device on - elif self.get_prop_value(prop=self._prop_on) is False: - await self.set_property_async(prop=self._prop_on, value=True) - # set mode - if self._prop_mode is None: - return - 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( - prop=self._prop_mode, value=mode_value): - raise RuntimeError( - f'set climate prop.mode failed, {hvac_mode}, {self.entity_id}') + await self.set_property_async( + prop=self._prop_on, + value=False if hvac_mode == HVACMode.OFF else True) @property def hvac_mode(self) -> Optional[HVACMode]: """The current hvac mode.""" - if self.get_prop_value(prop=self._prop_on) is False: - return HVACMode.OFF - return (self.get_map_value(map_=self._hvac_mode_map, - key=self.get_prop_value( - prop=self._prop_mode)) - if self._prop_mode else None) + return (HVACMode.AUTO if self.get_prop_value( + prop=self._prop_on) else HVACMode.OFF) class ElectricBlanket(FeatureOnOff, FeatureTargetTemperature, diff --git a/custom_components/xiaomi_home/cover.py b/custom_components/xiaomi_home/cover.py index dc738ca..49c4953 100644 --- a/custom_components/xiaomi_home/cover.py +++ b/custom_components/xiaomi_home/cover.py @@ -47,7 +47,7 @@ Cover entities for Xiaomi Home. """ from __future__ import annotations import logging -from typing import Optional +from typing import Any, Optional from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -107,7 +107,11 @@ class Cover(MIoTServiceEntity, CoverEntity): _prop_status_closed: Optional[list[int]] _prop_current_position: Optional[MIoTSpecProperty] _prop_target_position: Optional[MIoTSpecProperty] + _prop_position_value_min: Optional[int] + _prop_position_value_max: Optional[int] _prop_position_value_range: Optional[int] + _prop_pos_closing: bool + _prop_pos_opening: bool def __init__(self, miot_device: MIoTDevice, entity_data: MIoTEntityData, @@ -130,7 +134,11 @@ class Cover(MIoTServiceEntity, CoverEntity): self._prop_status_closed = [] self._prop_current_position = None self._prop_target_position = None + self._prop_position_value_min = None + self._prop_position_value_max = None self._prop_position_value_range = None + self._prop_pos_closing = False + self._prop_pos_opening = False self._close_threshold = close_threshold self._open_threshold = open_threshold @@ -177,6 +185,8 @@ class Cover(MIoTServiceEntity, CoverEntity): 'invalid current-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_range = (prop.value_range.max_ - prop.value_range.min_) self._prop_current_position = prop @@ -186,23 +196,52 @@ class Cover(MIoTServiceEntity, CoverEntity): '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_range = (prop.value_range.max_ - prop.value_range.min_) self._attr_supported_features |= CoverEntityFeature.SET_POSITION self._prop_target_position = prop + # For the device that has the current position property but no status + # property, the current position property will be used to determine the + # opening and the closing status. + if (self._prop_status is None) and (self._prop_current_position + is not None): + self.sub_prop_changed(self._prop_current_position, + self._position_changed_handler) + + def _position_changed_handler(self, prop: MIoTSpecProperty, + ctx: Any) -> None: + self._prop_pos_closing = False + self._prop_pos_opening = False + self.async_write_ha_state() async def async_open_cover(self, **kwargs) -> None: """Open the cover.""" + current = None if (self._prop_current_position + is None) else self.get_prop_value( + prop=self._prop_current_position) + if (current is not None) and (current < self._prop_position_value_max): + self._prop_pos_opening = True + self._prop_pos_closing = False await self.set_property_async(self._prop_motor_control, self._prop_motor_value_open) async def async_close_cover(self, **kwargs) -> None: """Close the cover.""" + current = None if (self._prop_current_position + is None) else self.get_prop_value( + prop=self._prop_current_position) + if (current is not None) and (current > self._prop_position_value_min): + self._prop_pos_opening = False + self._prop_pos_closing = True await self.set_property_async(self._prop_motor_control, self._prop_motor_value_close) async def async_stop_cover(self, **kwargs) -> None: """Stop the cover.""" + self._prop_pos_opening = False + self._prop_pos_closing = False await self.set_property_async(self._prop_motor_control, self._prop_motor_value_pause) @@ -211,6 +250,10 @@ class Cover(MIoTServiceEntity, CoverEntity): pos = kwargs.get(ATTR_POSITION, None) if pos is None: return None + current = self.current_cover_position + if current is not None: + self._prop_pos_opening = pos > current + self._prop_pos_closing = pos < current pos = round(pos * self._prop_position_value_range / 100) await self.set_property_async(prop=self._prop_target_position, value=pos) @@ -225,24 +268,14 @@ class Cover(MIoTServiceEntity, CoverEntity): # Assume that the current position is the same as the target # position when the current position is not defined in the device's # MIoT-Spec-V2. - pos = None if (self._prop_target_position is None) else self.get_prop_value( - prop=self._prop_target_position) - else: - pos = self.get_prop_value(prop=self._prop_current_position) - - if pos is None: - return None - - # Convert the position to a percentage - percentage = round(pos * 100 / self._prop_position_value_range) - - # Adjust the position to 0 if it is below the close threshold - if percentage <= self._close_threshold: - return 0 - # Adjust the position to 0 if it is below the close threshold - if percentage >= self._open_threshold: - return 100 - return percentage + if self._prop_target_position is None: + return None + self._prop_pos_opening = False + self._prop_pos_closing = False + return self.get_prop_value(prop=self._prop_target_position) + pos = self.get_prop_value(prop=self._prop_current_position) + return None if pos is None else round(pos * 100 / + self._prop_position_value_range) @property def is_opening(self) -> Optional[bool]: @@ -250,14 +283,9 @@ class Cover(MIoTServiceEntity, CoverEntity): if self._prop_status and self._prop_status_opening: return (self.get_prop_value(prop=self._prop_status) in self._prop_status_opening) - # The status is prior to the numerical relationship of the current - # position and the target position when determining whether the cover + # The status has higher priority when determining whether the cover # is opening. - if (self._prop_target_position and - self.current_cover_position is not None): - return (self.current_cover_position - < self.get_prop_value(prop=self._prop_target_position)) - return None + return self._prop_pos_opening @property def is_closing(self) -> Optional[bool]: @@ -265,14 +293,9 @@ class Cover(MIoTServiceEntity, CoverEntity): if self._prop_status and self._prop_status_closing: return (self.get_prop_value(prop=self._prop_status) in self._prop_status_closing) - # The status is prior to the numerical relationship of the current - # position and the target position when determining whether the cover + # The status has higher priority when determining whether the cover # is closing. - if (self._prop_target_position and - self.current_cover_position is not None): - return (self.current_cover_position - > self.get_prop_value(prop=self._prop_target_position)) - return None + return self._prop_pos_closing @property def is_closed(self) -> Optional[bool]: diff --git a/custom_components/xiaomi_home/fan.py b/custom_components/xiaomi_home/fan.py index 0f64aa8..1d15c9b 100644 --- a/custom_components/xiaomi_home/fan.py +++ b/custom_components/xiaomi_home/fan.py @@ -172,7 +172,7 @@ class Fan(MIoTServiceEntity, FanEntity): self._attr_supported_features |= FanEntityFeature.OSCILLATE self._prop_horizontal_swing = prop elif prop.name == 'wind-reverse': - if prop.format_ == 'bool': + if prop.format_ == bool: self._prop_wind_reverse_forward = False self._prop_wind_reverse_reverse = True elif prop.value_list: @@ -186,7 +186,7 @@ class Fan(MIoTServiceEntity, FanEntity): or self._prop_wind_reverse_reverse is None ): # NOTICE: Value may be 0 or False - _LOGGER.info( + _LOGGER.error( 'invalid wind-reverse, %s', self.entity_id) continue self._attr_supported_features |= FanEntityFeature.DIRECTION diff --git a/custom_components/xiaomi_home/manifest.json b/custom_components/xiaomi_home/manifest.json index 9ddd8dd..217ef07 100644 --- a/custom_components/xiaomi_home/manifest.json +++ b/custom_components/xiaomi_home/manifest.json @@ -20,7 +20,7 @@ ], "requirements": [ "construct>=2.10.56", - "paho-mqtt<2.0.0", + "paho-mqtt", "numpy", "cryptography", "psutil" @@ -29,4 +29,4 @@ "zeroconf": [ "_miot-central._tcp.local." ] -} \ No newline at end of file +} diff --git a/custom_components/xiaomi_home/miot/miot_device.py b/custom_components/xiaomi_home/miot/miot_device.py index 1f3f186..b91be48 100644 --- a/custom_components/xiaomi_home/miot/miot_device.py +++ b/custom_components/xiaomi_home/miot/miot_device.py @@ -549,6 +549,10 @@ class MIoTDevice: # Optional actions # Optional events miot_service.platform = platform + # entity_category + if entity_category := SPEC_SERVICE_TRANS_MAP[service_name].get( + 'entity_category', None): + miot_service.entity_category = entity_category return entity_data def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool: @@ -899,6 +903,7 @@ class MIoTServiceEntity(Entity): self._attr_name = ( f'{"* "if self.entity_data.spec.proprietary else " "}' f'{self.entity_data.spec.description_trans}') + self._attr_entity_category = entity_data.spec.entity_category # Set entity attr self._attr_unique_id = self.entity_id self._attr_should_poll = False diff --git a/custom_components/xiaomi_home/miot/miot_spec.py b/custom_components/xiaomi_home/miot/miot_spec.py index 859453b..fca22a7 100644 --- a/custom_components/xiaomi_home/miot/miot_spec.py +++ b/custom_components/xiaomi_home/miot/miot_spec.py @@ -56,7 +56,7 @@ 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 .common import MIoTHttp, load_yaml_file, load_json_file from .miot_error import MIoTSpecError from .miot_storage import MIoTStorage @@ -465,7 +465,7 @@ class _MIoTSpecBase: iid: int type_: str description: str - description_trans: str + description_trans: Optional[str] proprietary: bool need_filter: bool name: str @@ -476,6 +476,7 @@ class _MIoTSpecBase: device_class: Any state_class: Any external_unit: Any + entity_category: Optional[str] spec_id: int @@ -494,6 +495,7 @@ class _MIoTSpecBase: self.device_class = None self.state_class = None self.external_unit = None + self.entity_category = None self.spec_id = hash(f'{self.type_}.{self.iid}') @@ -837,6 +839,7 @@ class _MIoTSpecMultiLang: """MIoT SPEC multi lang class.""" # pylint: disable=broad-exception-caught _DOMAIN: str = 'miot_specs_multi_lang' + _MULTI_LANG_FILE = 'specs/multi_lang.json' _lang: str _storage: MIoTStorage _main_loop: asyncio.AbstractEventLoop @@ -892,6 +895,25 @@ class _MIoTSpecMultiLang: except Exception as err: trans_local = {} _LOGGER.info('get multi lang from local failed, %s, %s', urn, err) + # Revert: load multi_lang.json + try: + trans_local_json = 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)) + urn_strs: list[str] = urn.split(':') + urn_key: str = ':'.join(urn_strs[:6]) + if ( + isinstance(trans_local_json, dict) + and urn_key in trans_local_json + and self._lang in trans_local_json[urn_key] + ): + trans_cache.update(trans_local_json[urn_key][self._lang]) + trans_local = trans_local_json[urn_key] + except Exception as err: # pylint: disable=broad-exception-caught + _LOGGER.error('multi lang, load json file error, %s', err) + # Revert end # Default language if not trans_cache: if trans_cloud and DEFAULT_INTEGRATION_LANGUAGE in trans_cloud: @@ -1205,6 +1227,13 @@ class _SpecModify: return None return value_range + def get_prop_value_list(self, siid: int, piid: int) -> Optional[list]: + value_list = self.__get_prop_item(siid=siid, piid=piid, + key='value-list') + if not isinstance(value_list, list): + return None + return value_list + def __get_prop_item(self, siid: int, piid: int, key: str) -> Optional[str]: if not self._selected: return None @@ -1444,10 +1473,12 @@ class MIoTSpecParser: key=':'.join(p_type_strs[:5])) or property_['description'] or spec_prop.name) - if 'value-range' in property_: - spec_prop.value_range = property_['value-range'] - elif 'value-list' in property_: - v_list: list[dict] = property_['value-list'] + # Modify value-list before translation + v_list: list[dict] = self._spec_modify.get_prop_value_list( + siid=service['iid'], piid=property_['iid']) + if (v_list is None) and ('value-list' in property_): + v_list = property_['value-list'] + if v_list is not None: for index, v in enumerate(v_list): if v['description'].strip() == '': v['description'] = f'v_{v["value"]}' @@ -1461,6 +1492,8 @@ class MIoTSpecParser: f'{v["description"]}') or v['name']) spec_prop.value_list = MIoTSpecValueList.from_spec(v_list) + if 'value-range' in property_: + spec_prop.value_range = property_['value-range'] elif property_['format'] == 'bool': v_tag = ':'.join(p_type_strs[:5]) v_descriptions = ( diff --git a/custom_components/xiaomi_home/miot/specs/multi_lang.json b/custom_components/xiaomi_home/miot/specs/multi_lang.json new file mode 100644 index 0000000..7f16732 --- /dev/null +++ b/custom_components/xiaomi_home/miot/specs/multi_lang.json @@ -0,0 +1,172 @@ +{ + "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" + }, + "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": { + "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": "右键确认" + } + } +} \ No newline at end of file diff --git a/custom_components/xiaomi_home/miot/specs/spec_modify.yaml b/custom_components/xiaomi_home/miot/specs/spec_modify.yaml index 131ef59..58ba8ef 100644 --- a/custom_components/xiaomi_home/miot/specs/spec_modify.yaml +++ b/custom_components/xiaomi_home/miot/specs/spec_modify.yaml @@ -49,3 +49,12 @@ urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1: - 1 - 1 urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:2: urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1 +urn:miot-spec-v2:device:bath-heater:0000A028:opple-acmoto:1: + prop.5.2: + value-list: + - value: 1 + description: low + - value: 128 + description: medium + - value: 255 + description: high diff --git a/custom_components/xiaomi_home/miot/specs/specv2entity.py b/custom_components/xiaomi_home/miot/specs/specv2entity.py index 82b2844..b3039de 100644 --- a/custom_components/xiaomi_home/miot/specs/specv2entity.py +++ b/custom_components/xiaomi_home/miot/specs/specv2entity.py @@ -51,6 +51,7 @@ from homeassistant.components.event import EventDeviceClass from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + EntityCategory, LIGHT_LUX, UnitOfEnergy, UnitOfPower, @@ -330,7 +331,8 @@ SPEC_DEVICE_TRANS_MAP: dict = { 'events': set, 'actions': set }, - 'entity': str + 'entity': str, + 'entity_category'?: str } } """ @@ -348,10 +350,23 @@ SPEC_SERVICE_TRANS_MAP: dict = { }, 'entity': 'light' }, - 'indicator-light': 'light', 'ambient-light': 'light', 'night-light': 'light', 'white-light': 'light', + 'indicator-light': { + 'required': { + 'properties': { + 'on': {'read', 'write'} + } + }, + 'optional': { + 'properties': { + 'mode', 'brightness', + } + }, + 'entity': 'light', + 'entity_category': EntityCategory.CONFIG + }, 'fan': { 'required': { 'properties': {