mirror of
https://github.com/XiaoMi/ha_xiaomi_home.git
synced 2025-03-31 14:55:31 +08:00
1030 lines
40 KiB
Python
1030 lines
40 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright (C) 2024 Xiaomi Corporation.
|
|
|
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
|
Integration and related Xiaomi cloud service API interface provided under this
|
|
license, including source code and object code (collectively, "Licensed Work"),
|
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
|
distribute the Licensed Work only for your use of Home Assistant for
|
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
|
you to use the Licensed Work for any other purpose, including but not limited
|
|
to use Licensed Work to develop applications (APP), Web services, and other
|
|
forms of software.
|
|
|
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
|
modifications, whether in source or object form, provided that you must give
|
|
any other recipients of the Licensed Work a copy of this License and retain all
|
|
copyright and disclaimers.
|
|
|
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
|
for any direct, indirect, special, incidental, or consequential damages or
|
|
losses arising from the use or inability to use the Licensed Work.
|
|
|
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
|
without limitation, without obtaining other written permission from Xiaomi, you
|
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
|
may make the public associate with Xiaomi in any form to publicize or promote
|
|
the software or hardware devices that use the Licensed Work.
|
|
|
|
Xiaomi has the right to immediately terminate all your authorization under this
|
|
License in the event:
|
|
1. You assert patent invalidation, litigation, or other claims against patents
|
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
|
off Xiaomi or its affiliates' products.
|
|
|
|
MIoT-Spec-V2 parser.
|
|
"""
|
|
import asyncio
|
|
import json
|
|
import platform
|
|
import time
|
|
from typing import Optional
|
|
from urllib.parse import urlencode
|
|
from urllib.request import Request, urlopen
|
|
import logging
|
|
|
|
from .const import DEFAULT_INTEGRATION_LANGUAGE, SPEC_STD_LIB_EFFECTIVE_TIME
|
|
from .miot_error import MIoTSpecError
|
|
from .miot_storage import (
|
|
MIoTStorage,
|
|
SpecBoolTranslation,
|
|
SpecFilter,
|
|
SpecMultiLang)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class MIoTSpecBase:
|
|
"""MIoT SPEC base class."""
|
|
iid: int
|
|
type_: str
|
|
description: str
|
|
description_trans: Optional[str]
|
|
proprietary: bool
|
|
need_filter: bool
|
|
name: Optional[str]
|
|
|
|
# External params
|
|
platform: str
|
|
device_class: any
|
|
icon: str
|
|
external_unit: any
|
|
|
|
spec_id: str
|
|
|
|
def __init__(self, spec: dict) -> None:
|
|
self.iid = spec['iid']
|
|
self.type_ = spec['type']
|
|
self.description = spec['description']
|
|
|
|
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.platform = None
|
|
self.device_class = None
|
|
self.icon = None
|
|
self.external_unit = None
|
|
|
|
self.spec_id = hash(f'{self.type_}.{self.iid}')
|
|
|
|
def __hash__(self) -> int:
|
|
return self.spec_id
|
|
|
|
def __eq__(self, value: object) -> bool:
|
|
return self.spec_id == value.spec_id
|
|
|
|
|
|
class MIoTSpecProperty(MIoTSpecBase):
|
|
"""MIoT SPEC property class."""
|
|
format_: str
|
|
precision: int
|
|
unit: str
|
|
|
|
value_range: list
|
|
value_list: list[dict]
|
|
|
|
_access: list
|
|
_writable: bool
|
|
_readable: bool
|
|
_notifiable: bool
|
|
|
|
service: MIoTSpecBase
|
|
|
|
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
|
|
) -> None:
|
|
super().__init__(spec=spec)
|
|
self.service = service
|
|
self.format_ = format_
|
|
self.access = access
|
|
self.unit = unit
|
|
self.value_range = value_range
|
|
self.value_list = value_list
|
|
self.precision = precision
|
|
|
|
self.spec_id = hash(
|
|
f'p.{self.name}.{self.service.iid}.{self.iid}')
|
|
|
|
@property
|
|
def access(self) -> list:
|
|
return self._access
|
|
|
|
@access.setter
|
|
def access(self, value: list) -> None:
|
|
self._access = value
|
|
if isinstance(value, list):
|
|
self._writable = 'write' in value
|
|
self._readable = 'read' in value
|
|
self._notifiable = 'notify' in value
|
|
|
|
@property
|
|
def writable(self) -> bool:
|
|
return self._writable
|
|
|
|
@property
|
|
def readable(self) -> bool:
|
|
return self._readable
|
|
|
|
@property
|
|
def notifiable(self):
|
|
return self._notifiable
|
|
|
|
def value_format(self, value: any) -> any:
|
|
if value is None:
|
|
return None
|
|
if self.format_ == 'int':
|
|
return int(value)
|
|
if self.format_ == 'float':
|
|
return round(value, self.precision)
|
|
if self.format_ == 'bool':
|
|
return bool(value in [True, 1, 'true', '1'])
|
|
return value
|
|
|
|
def dump(self) -> dict:
|
|
return {
|
|
'type': self.type_,
|
|
'name': self.name,
|
|
'iid': self.iid,
|
|
'description': self.description,
|
|
'description_trans': self.description_trans,
|
|
'proprietary': self.proprietary,
|
|
'need_filter': self.need_filter,
|
|
'format': self.format_,
|
|
'access': self._access,
|
|
'unit': self.unit,
|
|
'value_range': self.value_range,
|
|
'value_list': self.value_list,
|
|
'precision': self.precision
|
|
}
|
|
|
|
|
|
class MIoTSpecEvent(MIoTSpecBase):
|
|
"""MIoT SPEC event class."""
|
|
argument: list[MIoTSpecProperty]
|
|
service: MIoTSpecBase
|
|
|
|
def __init__(
|
|
self, spec: dict, service: MIoTSpecBase = None,
|
|
argument: list[MIoTSpecProperty] = None
|
|
) -> None:
|
|
super().__init__(spec=spec)
|
|
self.argument = argument
|
|
self.service = service
|
|
|
|
self.spec_id = hash(
|
|
f'e.{self.name}.{self.service.iid}.{self.iid}')
|
|
|
|
def dump(self) -> dict:
|
|
return {
|
|
'type': self.type_,
|
|
'name': self.name,
|
|
'iid': self.iid,
|
|
'description': self.description,
|
|
'description_trans': self.description_trans,
|
|
'proprietary': self.proprietary,
|
|
'need_filter': self.need_filter,
|
|
'argument': [prop.iid for prop in self.argument],
|
|
}
|
|
|
|
|
|
class MIoTSpecAction(MIoTSpecBase):
|
|
"""MIoT SPEC action class."""
|
|
in_: list[MIoTSpecProperty]
|
|
out: list[MIoTSpecProperty]
|
|
service: MIoTSpecBase
|
|
|
|
def __init__(
|
|
self, spec: dict, service: MIoTSpecBase = None,
|
|
in_: list[MIoTSpecProperty] = None,
|
|
out: list[MIoTSpecProperty] = None
|
|
) -> None:
|
|
super().__init__(spec=spec)
|
|
self.in_ = in_
|
|
self.out = out
|
|
self.service = service
|
|
|
|
self.spec_id = hash(
|
|
f'a.{self.name}.{self.service.iid}.{self.iid}')
|
|
|
|
def dump(self) -> dict:
|
|
return {
|
|
'type': self.type_,
|
|
'name': self.name,
|
|
'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]
|
|
}
|
|
|
|
|
|
class MIoTSpecService(MIoTSpecBase):
|
|
"""MIoT SPEC service class."""
|
|
properties: list[MIoTSpecProperty]
|
|
events: list[MIoTSpecEvent]
|
|
actions: list[MIoTSpecAction]
|
|
|
|
def __init__(self, spec: dict) -> None:
|
|
super().__init__(spec=spec)
|
|
self.properties = []
|
|
self.events = []
|
|
self.actions = []
|
|
|
|
def dump(self) -> dict:
|
|
return {
|
|
'type': self.type_,
|
|
'name': self.name,
|
|
'iid': self.iid,
|
|
'description': self.description,
|
|
'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],
|
|
}
|
|
|
|
|
|
# @dataclass
|
|
class MIoTSpecInstance:
|
|
"""MIoT SPEC instance class."""
|
|
urn: str
|
|
name: str
|
|
# urn_name: str
|
|
description: str
|
|
description_trans: str
|
|
services: list[MIoTSpecService]
|
|
|
|
# External params
|
|
platform: str
|
|
device_class: any
|
|
icon: str
|
|
|
|
def __init__(
|
|
self, urn: str = None, name: str = None,
|
|
description: str = None, description_trans: str = None
|
|
) -> None:
|
|
self.urn = urn
|
|
self.name = name
|
|
self.description = description
|
|
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 = []
|
|
for service in specs['services']:
|
|
spec_service = MIoTSpecService(spec=service)
|
|
for prop in service['properties']:
|
|
spec_prop = MIoTSpecProperty(
|
|
spec=prop,
|
|
service=spec_service,
|
|
format_=prop['format'],
|
|
access=prop['access'],
|
|
unit=prop['unit'],
|
|
value_range=prop['value_range'],
|
|
value_list=prop['value_list'],
|
|
precision=prop.get('precision', 0))
|
|
spec_service.properties.append(spec_prop)
|
|
for event in service['events']:
|
|
spec_event = MIoTSpecEvent(
|
|
spec=event, service=spec_service)
|
|
arg_list: list[MIoTSpecProperty] = []
|
|
for piid in event['argument']:
|
|
for prop in spec_service.properties:
|
|
if prop.iid == piid:
|
|
arg_list.append(prop)
|
|
break
|
|
spec_event.argument = arg_list
|
|
spec_service.events.append(spec_event)
|
|
for action in service['actions']:
|
|
spec_action = MIoTSpecAction(
|
|
spec=action, service=spec_service, in_=action['in'])
|
|
in_list: list[MIoTSpecProperty] = []
|
|
for piid in action['in']:
|
|
for prop in spec_service.properties:
|
|
if prop.iid == piid:
|
|
in_list.append(prop)
|
|
break
|
|
spec_action.in_ = in_list
|
|
out_list: list[MIoTSpecProperty] = []
|
|
for piid in action['out']:
|
|
for prop in spec_service.properties:
|
|
if prop.iid == piid:
|
|
out_list.append(prop)
|
|
break
|
|
spec_action.out = out_list
|
|
spec_service.actions.append(spec_action)
|
|
self.services.append(spec_service)
|
|
return self
|
|
|
|
def dump(self) -> dict:
|
|
return {
|
|
'urn': self.urn,
|
|
'name': self.name,
|
|
'description': self.description,
|
|
'description_trans': self.description_trans,
|
|
'services': [service.dump() for service in self.services]
|
|
}
|
|
|
|
|
|
class SpecStdLib:
|
|
"""MIoT-Spec-V2 standard library."""
|
|
_lang: str
|
|
_spec_std_lib: Optional[dict[str, dict[str, dict[str, str]]]]
|
|
|
|
def __init__(self, lang: str) -> None:
|
|
self._lang = lang
|
|
self._spec_std_lib = None
|
|
|
|
def init(self, std_lib: 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._spec_std_lib = std_lib
|
|
|
|
def deinit(self) -> None:
|
|
self._spec_std_lib = None
|
|
|
|
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]
|
|
|
|
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 property_translate(self, key: str) -> Optional[str]:
|
|
if (
|
|
not self._spec_std_lib
|
|
or key not in self._spec_std_lib['properties']
|
|
):
|
|
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]
|
|
|
|
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]
|
|
|
|
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 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 dump(self) -> dict[str, dict[str, str]]:
|
|
return self._spec_std_lib
|
|
|
|
|
|
class MIoTSpecParser:
|
|
"""MIoT SPEC parser."""
|
|
VERSION: int = 1
|
|
DOMAIN: str = 'miot_specs'
|
|
_lang: str
|
|
_storage: MIoTStorage
|
|
_main_loop: asyncio.AbstractEventLoop
|
|
|
|
_init_done: bool
|
|
_ram_cache: dict
|
|
|
|
_std_lib: SpecStdLib
|
|
_bool_trans: SpecBoolTranslation
|
|
_multi_lang: SpecMultiLang
|
|
_spec_filter: SpecFilter
|
|
|
|
def __init__(
|
|
self, lang: str = DEFAULT_INTEGRATION_LANGUAGE,
|
|
storage: MIoTStorage = None,
|
|
loop: Optional[asyncio.AbstractEventLoop] = None
|
|
) -> None:
|
|
self._lang = lang
|
|
self._storage = storage
|
|
self._main_loop = loop or asyncio.get_running_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
|
|
# 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')
|
|
else:
|
|
if std_lib_cache:
|
|
self._std_lib.init(std_lib_cache['data'])
|
|
_LOGGER.error('get spec std lib failed, use local cache')
|
|
else:
|
|
_LOGGER.error('get spec std lib failed')
|
|
self._init_done = True
|
|
|
|
async def deinit_async(self) -> None:
|
|
self._init_done = False
|
|
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:
|
|
"""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)
|
|
# Retry three times
|
|
for index in range(3):
|
|
try:
|
|
return await self.__parse(urn=urn)
|
|
except Exception as err: # pylint: disable=broad-exception-caught
|
|
_LOGGER.error(
|
|
'parse error, retry, %d, %s, %s', index, urn, err)
|
|
return None
|
|
|
|
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')
|
|
else:
|
|
raise MIoTSpecError('get spec std lib failed')
|
|
success_count = 0
|
|
for index in range(0, len(urn_list), 5):
|
|
batch = urn_list[index:index+5]
|
|
task_list = [self._main_loop.create_task(
|
|
self.parse(urn=urn, skip_cache=True)) for urn in batch]
|
|
results = await asyncio.gather(*task_list)
|
|
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)
|
|
|
|
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
|
|
|
|
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(
|
|
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)
|
|
if (
|
|
not isinstance(instance, dict)
|
|
or 'type' not in instance
|
|
or 'description' not in instance
|
|
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)
|
|
# Parse device type
|
|
spec_instance: MIoTSpecInstance = MIoTSpecInstance(
|
|
urn=urn, name=urn_strs[3],
|
|
description=instance['description'],
|
|
description_trans=(
|
|
self._std_lib.device_translate(key=':'.join(urn_strs[:5]))
|
|
or instance['description']
|
|
or urn_strs[3]))
|
|
# Parse services
|
|
for service in instance.get('services', []):
|
|
if (
|
|
'iid' not in service
|
|
or 'type' not in service
|
|
or 'description' not in service
|
|
):
|
|
_LOGGER.error('invalid service, %s, %s', urn, service)
|
|
continue
|
|
type_strs: list[str] = service['type'].split(':')
|
|
if type_strs[3] == 'device-information':
|
|
# Ignore device-information service
|
|
continue
|
|
spec_service: MIoTSpecService = MIoTSpecService(spec=service)
|
|
spec_service.name = type_strs[3]
|
|
# Filter spec service
|
|
spec_service.need_filter = self._spec_filter.filter_service(
|
|
siid=service['iid'])
|
|
if type_strs[1] != 'miot-spec-v2':
|
|
spec_service.proprietary = True
|
|
spec_service.description_trans = (
|
|
translation.get(f's:{service["iid"]}', None)
|
|
or self._std_lib.service_translate(key=':'.join(type_strs[:5]))
|
|
or service['description']
|
|
or spec_service.name
|
|
)
|
|
# Parse service property
|
|
for property_ in service.get('properties', []):
|
|
if (
|
|
'iid' not in property_
|
|
or 'type' not in property_
|
|
or 'description' not in property_
|
|
or 'format' not in property_
|
|
or 'access' not in property_
|
|
):
|
|
continue
|
|
p_type_strs: list[str] = property_['type'].split(':')
|
|
spec_prop: MIoTSpecProperty = MIoTSpecProperty(
|
|
spec=property_,
|
|
service=spec_service,
|
|
format_=self.__spec_format2dtype(property_['format']),
|
|
access=property_['access'],
|
|
unit=property_.get('unit', None))
|
|
spec_prop.name = p_type_strs[3]
|
|
# Filter spec property
|
|
spec_prop.need_filter = (
|
|
spec_service.need_filter
|
|
or self._spec_filter.filter_property(
|
|
siid=service['iid'], piid=property_['iid']))
|
|
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)
|
|
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
|
|
elif 'value-list' in property_:
|
|
v_list: list[dict] = property_['value-list']
|
|
for index, v in enumerate(v_list):
|
|
v['name'] = v['description']
|
|
v['description'] = (
|
|
translation.get(
|
|
f'v:{service["iid"]}:{property_["iid"]}:'
|
|
f'{index}', None)
|
|
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
|
|
elif property_['format'] == 'bool':
|
|
v_tag = ':'.join(p_type_strs[:5])
|
|
v_descriptions: dict = (
|
|
await self._bool_trans.translate_async(urn=v_tag))
|
|
if v_descriptions:
|
|
spec_prop.value_list = v_descriptions
|
|
spec_service.properties.append(spec_prop)
|
|
# Parse service event
|
|
for event in service.get('events', []):
|
|
if (
|
|
'iid' not in event
|
|
or 'type' not in event
|
|
or 'description' not in event
|
|
or 'arguments' not in event
|
|
):
|
|
continue
|
|
e_type_strs: list[str] = event['type'].split(':')
|
|
spec_event: MIoTSpecEvent = MIoTSpecEvent(
|
|
spec=event, service=spec_service)
|
|
spec_event.name = e_type_strs[3]
|
|
# Filter spec event
|
|
spec_event.need_filter = (
|
|
spec_service.need_filter
|
|
or self._spec_filter.filter_event(
|
|
siid=service['iid'], eiid=event['iid']))
|
|
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)
|
|
or self._std_lib.event_translate(
|
|
key=':'.join(e_type_strs[:5]))
|
|
or event['description']
|
|
or spec_event.name
|
|
)
|
|
arg_list: list[MIoTSpecProperty] = []
|
|
for piid in event['arguments']:
|
|
for prop in spec_service.properties:
|
|
if prop.iid == piid:
|
|
arg_list.append(prop)
|
|
break
|
|
spec_event.argument = arg_list
|
|
spec_service.events.append(spec_event)
|
|
# Parse service action
|
|
for action in service.get('actions', []):
|
|
if (
|
|
'iid' not in action
|
|
or 'type' not in action
|
|
or 'description' not in action
|
|
or 'in' not in action
|
|
):
|
|
continue
|
|
a_type_strs: list[str] = action['type'].split(':')
|
|
spec_action: MIoTSpecAction = MIoTSpecAction(
|
|
spec=action, service=spec_service)
|
|
spec_action.name = a_type_strs[3]
|
|
# Filter spec action
|
|
spec_action.need_filter = (
|
|
spec_service.need_filter
|
|
or self._spec_filter.filter_action(
|
|
siid=service['iid'], aiid=action['iid']))
|
|
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)
|
|
or self._std_lib.action_translate(
|
|
key=':'.join(a_type_strs[:5]))
|
|
or action['description']
|
|
or spec_action.name
|
|
)
|
|
in_list: list[MIoTSpecProperty] = []
|
|
for piid in action['in']:
|
|
for prop in spec_service.properties:
|
|
if prop.iid == piid:
|
|
in_list.append(prop)
|
|
break
|
|
spec_action.in_ = in_list
|
|
out_list: list[MIoTSpecProperty] = []
|
|
for piid in action['out']:
|
|
for prop in spec_service.properties:
|
|
if prop.iid == piid:
|
|
out_list.append(prop)
|
|
break
|
|
spec_action.out = out_list
|
|
spec_service.actions.append(spec_action)
|
|
spec_instance.services.append(spec_service)
|
|
|
|
await self.__cache_set(urn=urn, data=spec_instance.dump())
|
|
return spec_instance
|