diff --git a/custom_components/xiaomi_miot/config_flow.py b/custom_components/xiaomi_miot/config_flow.py index 066396a72..72bc33fa3 100644 --- a/custom_components/xiaomi_miot/config_flow.py +++ b/custom_components/xiaomi_miot/config_flow.py @@ -344,6 +344,7 @@ async def async_step_cloud(self, user_input=None): vol.In(CLOUD_SERVERS), vol.Required(CONF_CONN_MODE, default=user_input.get(CONF_CONN_MODE, 'auto')): vol.In(CONN_MODES), + vol.Optional('trans_options', default=user_input.get('trans_options', False)): bool, vol.Optional('filter_models', default=user_input.get('filter_models', False)): bool, }) return self.async_show_form( @@ -662,6 +663,7 @@ async def async_step_cloud(self, user_input=None): vol.Required(CONF_CONN_MODE, default=user_input.get(CONF_CONN_MODE, DEFAULT_CONN_MODE)): vol.In(CONN_MODES), vol.Optional('renew_devices', default=user_input.get('renew_devices', False)): bool, + vol.Optional('trans_options', default=user_input.get('trans_options', False)): bool, vol.Optional('disable_message', default=user_input.get('disable_message', False)): bool, vol.Optional('disable_scene_history', default=user_input.get('disable_scene_history', False)): bool, }) @@ -690,6 +692,7 @@ async def async_step_cloud_filter(self, user_input=None): cfg = self.cloud.to_config() or {} cfg.update({ CONF_CONN_MODE: prev_input.get(CONF_CONN_MODE), + 'trans_options': prev_input.get('trans_options'), 'filter_models': prev_input.get('filter_models'), 'disable_message': prev_input.get('disable_message'), 'disable_scene_history': prev_input.get('disable_scene_history'), diff --git a/custom_components/xiaomi_miot/core/device.py b/custom_components/xiaomi_miot/core/device.py index 4c925f542..e24269bb4 100644 --- a/custom_components/xiaomi_miot/core/device.py +++ b/custom_components/xiaomi_miot/core/device.py @@ -274,8 +274,9 @@ async def get_spec(self) -> Optional[MiotSpec]: dat = self.hass.data[DOMAIN].setdefault('miot_specs', {}) obj = dat.get(self.model) if not obj: + trans_options = self.custom_config_bool('trans_options', self.entry.get_config('trans_options')) urn = await self.get_urn() - obj = await MiotSpec.async_from_type(self.hass, urn) + obj = await MiotSpec.async_from_type(self.hass, urn, trans_options=trans_options) dat[self.model] = obj if obj: self.spec = copy.copy(obj) diff --git a/custom_components/xiaomi_miot/core/miot_spec.py b/custom_components/xiaomi_miot/core/miot_spec.py index 22ae10045..0f441c577 100644 --- a/custom_components/xiaomi_miot/core/miot_spec.py +++ b/custom_components/xiaomi_miot/core/miot_spec.py @@ -4,7 +4,9 @@ import random import time import re +from functools import cached_property +from homeassistant.core import HomeAssistant from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, @@ -30,6 +32,7 @@ DOMAIN, TRANSLATION_LANGUAGES, ) +from .utils import get_translation_langs _LOGGER = logging.getLogger(__name__) @@ -104,7 +107,7 @@ def name_by_type(typ): def translation_keys(self): return ['_globals'] - @property + @cached_property def translations(self): dic = TRANSLATION_LANGUAGES kls = self.translation_keys @@ -115,7 +118,7 @@ def translations(self): dic = {**dic, **d} return dic - def get_translation(self, des): + def get_translation(self, des, viid=None): dls = [ des.lower(), des, @@ -132,6 +135,11 @@ def get_translation(self, des): continue ret = ret[d] return ret + + fun = getattr(self, 'get_spec_translation', None) + ret = fun(viid=viid) if fun else None + if ret: + return ret return des @staticmethod @@ -150,7 +158,10 @@ def __repr__(self): # https://iot.mi.com/new/doc/design/spec/xiaoai class MiotSpec(MiotSpecInstance): - def __init__(self, dat: dict): + def __init__(self, hass: HomeAssistant, dat: dict, translations=None, trans_options=None): + self.hass = hass + self.trans_options = trans_options + self.spec_translations = translations or {} super().__init__(dat) self.services = {} self.services_count = {} @@ -254,6 +265,18 @@ def generate_entity_id(self, entity, suffix=None, domain=None): domain = DOMAIN return f'{domain}.{eid}' + def get_spec_translation(self, siid, piid=None, aiid=None, viid=None): + if viid is not None and not self.trans_options: + return None + key = MiotSpec.spec_lang_key(siid, piid, aiid, viid) + langs = get_translation_langs(self.hass, self.spec_translations.keys()) + for lang in langs: + dic = self.spec_translations.get(lang) or {} + val = dic.get(key) + if val is not None: + return val + return None + @staticmethod async def async_from_model(hass, model, use_remote=False): typ = await MiotSpec.async_get_model_type(hass, model, use_remote) @@ -317,7 +340,7 @@ async def async_get_model_type(hass, model, use_remote=False): return typ @staticmethod - async def async_from_type(hass, typ): + async def async_from_type(hass, typ, trans_options=False): if not typ: return None fnm = f'{DOMAIN}/{typ}.json' @@ -354,7 +377,9 @@ async def async_from_type(hass, typ): } await store.async_save(dat) _LOGGER.warning('Get miot-spec for %s failed: %s', typ, exc) - return MiotSpec(dat) + + translations = await MiotSpec.async_get_langs(hass, typ) + return MiotSpec(hass, dat, translations, trans_options=trans_options) @staticmethod def unique_prop(siid, piid=None, aiid=None, eiid=None, valid=False): @@ -375,6 +400,57 @@ def unique_prop(siid, piid=None, aiid=None, eiid=None, valid=False): return None return f'{typ}.{siid}.{iid}' + @staticmethod + def spec_lang_key(siid, piid=None, aiid=None, viid=None): + key = f'service:{siid:03}' + if aiid is not None: + return f'{key}:action:{aiid:03}' + if piid is not None: + key = f'{key}:property:{piid:03}' + if viid is not None: + key = f'{key}:valuelist:{viid:03}' + return key + + @staticmethod + async def async_get_langs(hass, typ): + if not typ: + return None + fnm = f'{DOMAIN}/spec-langs/{typ}.json' + if platform.system() == 'Windows': + fnm = fnm.replace(':', '_') + store = Store(hass, 1, fnm) + try: + cached = await store.async_load() or {} + except (ValueError, HomeAssistantError): + await store.async_remove() + cached = {} + dat = cached + ptm = dat.pop('_updated_time', 0) + now = int(time.time()) + ttl = 60 + if dat.get('data'): + ttl = 86400 * random.randint(30, 50) + if dat and now - ptm > ttl: + dat = {} + if not dat.get('type'): + try: + url = f'/instance/v2/multiLanguage?urn={typ}' + dat = await MiotSpec.async_download_miot_spec(hass, url, tries=3) + dat['_updated_time'] = now + await store.async_save(dat) + except (TypeError, ValueError, BaseException) as exc: + if cached: + dat = cached + else: + dat = { + 'type': typ or 'unknown', + 'exception': f'{exc}', + '_updated_time': now, + } + await store.async_save(dat) + _LOGGER.warning('Get miot-spec langs for %s failed: %s', typ, exc) + return dat.get('data') or {} + @staticmethod async def async_download_miot_spec(hass, path, tries=1, timeout=30): session = async_get_clientsession(hass) @@ -537,6 +613,9 @@ def unique_prop(self, **kwargs): def generate_entity_id(self, entity, domain=None): return self.spec.generate_entity_id(entity, self.desc_name, domain) + def get_spec_translation(self, piid=None, aiid=None, viid=None): + return self.spec.get_spec_translation(self.iid, piid=piid, aiid=aiid, viid=viid) + @property def translation_keys(self): return ['_globals', self.name] @@ -633,6 +712,9 @@ def generate_entity_id(self, entity, domain=None): eid = re.sub(r'_(\d(?:_|$))', r'\1', eid) # issue#153 return eid + def get_spec_translation(self, viid=None): + return self.service.get_spec_translation(piid=self.iid, viid=viid) + @property def translation_keys(self): return [ @@ -679,12 +761,13 @@ def list_value(self, des): def list_description(self, val): rls = [] for v in self.value_list: - des = self.get_translation(v.get('description')) + vid = v.get('value') + des = self.get_translation(v.get('description'), viid=vid) if val is None: if des == '': - des = v.get('value') + des = vid rls.append(str(des)) - elif val == v.get('value'): + elif val == vid: return des if rls and val is None: return rls @@ -977,6 +1060,9 @@ def out_results(self, out=None): return dict(zip(kls, out)) return None + def get_spec_translation(self, viid=None): + return self.service.get_spec_translation(aiid=self.iid) + @property def translation_keys(self): return [ diff --git a/custom_components/xiaomi_miot/core/utils.py b/custom_components/xiaomi_miot/core/utils.py index 816a6dc93..cc58a6e37 100644 --- a/custom_components/xiaomi_miot/core/utils.py +++ b/custom_components/xiaomi_miot/core/utils.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity +from homeassistant.util import language as language_util from homeassistant.util.dt import DEFAULT_TIME_ZONE, get_time_zone from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -174,6 +175,14 @@ def get_translations(*keys): dic.update(tls) return dic +def get_translation_langs(hass: HomeAssistant, langs=None): + lang = hass.config.language + if not langs: + return lang + if 'en' not in langs: + langs.append('en') + return language_util.matches(lang, langs) + def is_offline_exception(exc): err = f'{exc}' diff --git a/custom_components/xiaomi_miot/translations/en.json b/custom_components/xiaomi_miot/translations/en.json index ac3636945..d5edb9322 100644 --- a/custom_components/xiaomi_miot/translations/en.json +++ b/custom_components/xiaomi_miot/translations/en.json @@ -25,6 +25,7 @@ "captcha": "Captcha", "server_country": "Server location of MiCloud", "conn_mode": "Connection mode for device", + "trans_options": "Translation property value description", "filter_models": "Filter devices via model/home/WiFi (Advanced)" } }, @@ -154,6 +155,7 @@ "server_country": "Server location of MiCloud", "conn_mode": "Connection mode for device", "renew_devices": "Force renew devices", + "trans_options": "Translation property value description", "disable_message": "Disable Mihome notification sensor", "disable_scene_history": "Disable Mihome scene history sensor" } diff --git a/custom_components/xiaomi_miot/translations/zh-Hans.json b/custom_components/xiaomi_miot/translations/zh-Hans.json index c381ae63f..376595bb3 100644 --- a/custom_components/xiaomi_miot/translations/zh-Hans.json +++ b/custom_components/xiaomi_miot/translations/zh-Hans.json @@ -25,6 +25,7 @@ "captcha": "验证码", "server_country": "小米服务器", "conn_mode": "设备连接模式", + "trans_options": "翻译属性值描述", "filter_models": "通过型号/家庭/WiFi筛选设备 (高级模式,新手勿选)" } }, @@ -93,6 +94,7 @@ "server_country": "小米服务器", "conn_mode": "设备连接模式", "renew_devices": "更新设备列表", + "trans_options": "翻译属性值描述", "disable_message": "禁用米家APP通知消息实体", "disable_scene_history": "禁用米家场景历史实体" }