feat: update std_lib and multi_lang logic

This commit is contained in:
topsworld 2025-01-08 15:23:31 +08:00
parent e4dfdf68ab
commit 078adfbd4c
2 changed files with 142 additions and 150 deletions

View File

@ -59,8 +59,7 @@ from .miot_error import MIoTSpecError
from .miot_storage import ( from .miot_storage import (
MIoTStorage, MIoTStorage,
SpecBoolTranslation, SpecBoolTranslation,
SpecFilter, SpecFilter)
SpecMultiLang)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -693,21 +692,16 @@ class MIoTSpecInstance:
} }
class MIoTSpecParser: class _MIoTSpecMultiLang:
"""MIoT SPEC parser.""" """MIoT SPEC multi lang class."""
# pylint: disable=inconsistent-quotes # pylint: disable=broad-exception-caught
VERSION: int = 1 _DOMAIN: str = 'miot_specs_multi_lang'
DOMAIN: str = 'miot_specs'
_lang: str _lang: str
_storage: MIoTStorage _storage: MIoTStorage
_main_loop: asyncio.AbstractEventLoop _main_loop: asyncio.AbstractEventLoop
_init_done: bool _custom_cache: dict[str, dict]
_current_data: Optional[dict[str, str]]
_std_lib: _SpecStdLib
_bool_trans: SpecBoolTranslation
_multi_lang: SpecMultiLang
_spec_filter: SpecFilter
def __init__( def __init__(
self, lang: Optional[str], self, lang: Optional[str],
@ -718,22 +712,137 @@ class MIoTSpecParser:
self._storage = storage self._storage = storage
self._main_loop = loop or asyncio.get_running_loop() 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 DEFAULT_INTEGRATION_LANGUAGE in trans_cloud:
trans_cache = trans_cloud[DEFAULT_INTEGRATION_LANGUAGE]
if 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 MIoTSpecParser:
"""MIoT SPEC parser."""
# pylint: disable=inconsistent-quotes
VERSION: int = 1
_DOMAIN: str = 'miot_specs'
_lang: str
_storage: MIoTStorage
_main_loop: asyncio.AbstractEventLoop
_std_lib: _SpecStdLib
_multi_lang: _MIoTSpecMultiLang
_init_done: bool
_bool_trans: SpecBoolTranslation
_spec_filter: SpecFilter
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._std_lib = _SpecStdLib(lang=self._lang)
self._multi_lang = _MIoTSpecMultiLang(
lang=self._lang, storage=self._storage, loop=self._main_loop)
self._init_done = False self._init_done = False
self._std_lib = _SpecStdLib(lang=self._lang)
self._bool_trans = SpecBoolTranslation( self._bool_trans = SpecBoolTranslation(
lang=self._lang, loop=self._main_loop) 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) self._spec_filter = SpecFilter(loop=self._main_loop)
async def init_async(self) -> None: async def init_async(self) -> None:
if self._init_done is True: if self._init_done is True:
return return
await self._bool_trans.init_async() await self._bool_trans.init_async()
await self._multi_lang.init_async()
await self._spec_filter.init_async() await self._spec_filter.init_async()
std_lib_cache = await self._storage.load_async( std_lib_cache = await self._storage.load_async(
domain=self.DOMAIN, name='spec_std_lib', type_=dict) domain=self._DOMAIN, name='spec_std_lib', type_=dict)
if ( if (
isinstance(std_lib_cache, dict) isinstance(std_lib_cache, dict)
and 'data' in std_lib_cache and 'data' in std_lib_cache
@ -751,7 +860,7 @@ class MIoTSpecParser:
# Update spec std lib # Update spec std lib
if await self._std_lib.refresh_async(): if await self._std_lib.refresh_async():
if not await self._storage.save_async( if not await self._storage.save_async(
domain=self.DOMAIN, name='spec_std_lib', domain=self._DOMAIN, name='spec_std_lib',
data={ data={
'data': self._std_lib.dump(), 'data': self._std_lib.dump(),
'ts': int(time.time()) 'ts': int(time.time())
@ -770,7 +879,6 @@ class MIoTSpecParser:
self._init_done = False self._init_done = False
# self._std_lib.deinit() # self._std_lib.deinit()
await self._bool_trans.deinit_async() await self._bool_trans.deinit_async()
await self._multi_lang.deinit_async()
await self._spec_filter.deinit_async() await self._spec_filter.deinit_async()
async def parse( async def parse(
@ -797,7 +905,7 @@ class MIoTSpecParser:
return False return False
if await self._std_lib.refresh_async(): if await self._std_lib.refresh_async():
if not await self._storage.save_async( if not await self._storage.save_async(
domain=self.DOMAIN, name='spec_std_lib', domain=self._DOMAIN, name='spec_std_lib',
data={ data={
'data': self._std_lib.dump(), 'data': self._std_lib.dump(),
'ts': int(time.time()) 'ts': int(time.time())
@ -819,7 +927,7 @@ class MIoTSpecParser:
if platform.system() == 'Windows': if platform.system() == 'Windows':
urn = urn.replace(':', '_') urn = urn.replace(':', '_')
return await self._storage.load_async( return await self._storage.load_async(
domain=self.DOMAIN, domain=self._DOMAIN,
name=f'{urn}_{self._lang}', name=f'{urn}_{self._lang}',
type_=dict) # type: ignore type_=dict) # type: ignore
@ -827,7 +935,7 @@ class MIoTSpecParser:
if platform.system() == 'Windows': if platform.system() == 'Windows':
urn = urn.replace(':', '_') urn = urn.replace(':', '_')
return await self._storage.save_async( return await self._storage.save_async(
domain=self.DOMAIN, name=f'{urn}_{self._lang}', data=data) domain=self._DOMAIN, name=f'{urn}_{self._lang}', data=data)
def __spec_format2dtype(self, format_: str) -> str: def __spec_format2dtype(self, format_: str) -> str:
# 'string'|'bool'|'uint8'|'uint16'|'uint32'| # 'string'|'bool'|'uint8'|'uint16'|'uint32'|
@ -840,11 +948,6 @@ class MIoTSpecParser:
url='https://miot-spec.org/miot-spec-v2/instance', url='https://miot-spec.org/miot-spec-v2/instance',
params={'type': urn}) params={'type': urn})
async def __get_translation(self, urn: str) -> Optional[dict]:
return await MIoTHttp.get_json_async(
url='https://miot-spec.org/instance/v2/multiLanguage',
params={'urn': urn})
async def __parse(self, urn: str) -> MIoTSpecInstance: async def __parse(self, urn: str) -> MIoTSpecInstance:
_LOGGER.debug('parse urn, %s', urn) _LOGGER.debug('parse urn, %s', urn)
# Load spec instance # Load spec instance
@ -856,68 +959,11 @@ class MIoTSpecParser:
or 'services' not in instance or 'services' not in instance
): ):
raise MIoTSpecError(f'invalid urn instance, {urn}') raise MIoTSpecError(f'invalid urn instance, {urn}')
translation: dict = {}
urn_strs: list[str] = urn.split(':') urn_strs: list[str] = urn.split(':')
urn_key: str = ':'.join(urn_strs[:6]) urn_key: str = ':'.join(urn_strs[:6])
try: # Set translation cache
# Load multiple language configuration. await self._multi_lang.set_spec_async(urn=urn_key)
res_trans = await self.__get_translation(urn=urn) # Set spec filter
if (
not isinstance(res_trans, dict)
or 'data' not in res_trans
or not isinstance(res_trans['data'], dict)
):
raise MIoTSpecError('invalid translation data')
trans_data: dict[str, str] = {}
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) self._spec_filter.filter_spec(urn_key=urn_key)
# Parse device type # Parse device type
spec_instance: MIoTSpecInstance = MIoTSpecInstance( spec_instance: MIoTSpecInstance = MIoTSpecInstance(
@ -948,7 +994,7 @@ class MIoTSpecParser:
if type_strs[1] != 'miot-spec-v2': if type_strs[1] != 'miot-spec-v2':
spec_service.proprietary = True spec_service.proprietary = True
spec_service.description_trans = ( 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 self._std_lib.service_translate(key=':'.join(type_strs[:5]))
or service['description'] or service['description']
or spec_service.name or spec_service.name
@ -979,8 +1025,8 @@ class MIoTSpecParser:
if p_type_strs[1] != 'miot-spec-v2': if p_type_strs[1] != 'miot-spec-v2':
spec_prop.proprietary = spec_service.proprietary or True spec_prop.proprietary = spec_service.proprietary or True
spec_prop.description_trans = ( spec_prop.description_trans = (
translation.get( self._multi_lang.translate(
f'p:{service["iid"]}:{property_["iid"]}', None) f'p:{service["iid"]}:{property_["iid"]}')
or self._std_lib.property_translate( or self._std_lib.property_translate(
key=':'.join(p_type_strs[:5])) key=':'.join(p_type_strs[:5]))
or property_['description'] or property_['description']
@ -1000,9 +1046,9 @@ class MIoTSpecParser:
for index, v in enumerate(v_list): for index, v in enumerate(v_list):
v['name'] = v['description'] v['name'] = v['description']
v['description'] = ( v['description'] = (
translation.get( self._multi_lang.translate(
f'v:{service["iid"]}:{property_["iid"]}:' f'v:{service["iid"]}:{property_["iid"]}:'
f'{index}', None) f'{index}')
or self._std_lib.value_translate( or self._std_lib.value_translate(
key=f'{type_strs[:5]}|{p_type_strs[3]}|' key=f'{type_strs[:5]}|{p_type_strs[3]}|'
f'{v["description"]}') f'{v["description"]}')
@ -1037,8 +1083,8 @@ class MIoTSpecParser:
if e_type_strs[1] != 'miot-spec-v2': if e_type_strs[1] != 'miot-spec-v2':
spec_event.proprietary = spec_service.proprietary or True spec_event.proprietary = spec_service.proprietary or True
spec_event.description_trans = ( spec_event.description_trans = (
translation.get( self._multi_lang.translate(
f'e:{service["iid"]}:{event["iid"]}', None) f'e:{service["iid"]}:{event["iid"]}')
or self._std_lib.event_translate( or self._std_lib.event_translate(
key=':'.join(e_type_strs[:5])) key=':'.join(e_type_strs[:5]))
or event['description'] or event['description']
@ -1073,8 +1119,8 @@ class MIoTSpecParser:
if a_type_strs[1] != 'miot-spec-v2': if a_type_strs[1] != 'miot-spec-v2':
spec_action.proprietary = spec_service.proprietary or True spec_action.proprietary = spec_service.proprietary or True
spec_action.description_trans = ( spec_action.description_trans = (
translation.get( self._multi_lang.translate(
f'a:{service["iid"]}:{action["iid"]}', None) f'a:{service["iid"]}:{action["iid"]}')
or self._std_lib.action_translate( or self._std_lib.action_translate(
key=':'.join(a_type_strs[:5])) key=':'.join(a_type_strs[:5]))
or action['description'] or action['description']

View File

@ -719,60 +719,6 @@ class MIoTCert:
return binascii.hexlify(sha1_hash.finalize()).decode('utf-8') 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: class SpecBoolTranslation:
""" """
Boolean value translation. Boolean value translation.