Paul Shawn 8778b00c3a
feat: support modify spec and value conversion (#663)
* fix: fix miot_device type error

* fix: fix type error

* feat: remove spec cache storage

* feat: update std_lib and multi_lang logic

* feat: update entity value-range

* feat: update value-list logic

* feat: update prop.format_ logic

* fix: fix miot cloud log error

* fix: fix fan entity

* style: ignore type error

* style: rename spec_filter func name

* feat: move bool_trans from storage to spec

* feat: move sepc_filter from storage to spec, use the YAML format file

* feat: same prop supports multiple sub

* feat: same event supports multiple sub

* fix: fix device remove error

* feat: add func slugify_did

* fix: fix multi lang error

* feat: update action debug logic

* feat: ignore normal disconnect log

* feat: support binary mode

* feat: change miot spec name type define

* style: ignore i18n tranlate type error

* fix: fix pylint warning

* fix: miot storage type error

* feat: support binary display mode configure

* feat: set default sensor state_class

* fix: fix sensor entity type error

* fix: fix __init__ type error

* feat: update test case logic

* fix: github actions add dependencies lib

* fix: fix some type error

* feat: update device list changed notify logic

* feat: update prop expr logic

* feat: add spec modify

* feat: update device sub id logic

* feat: update get miot client instance logic

* fix: fix some type error

* feat: update miot device unit and icon trans

* perf: update spec trans entity logic

* feat: update spec trans entity rule

* feat: update spec_modify

* feat: update sensor ENUM icon

* fix: fix miot device error

* fix: fix miot spec error

* featL update format check and spec modify file

* feat: update checkout rule format

* feat: handle special property.unit

* feat: add expr for cuco-cp1md

* feat: fix climate hvac error

* feat: set sensor suggested display precision

* feat: update climate set hvac logic

* feat: add expr for cuco-v3

* feat: update spec expr for chuangmi-212a01
2025-01-22 19:21:02 +08:00

818 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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 http client.
"""
import asyncio
import base64
import hashlib
import json
import logging
import re
import time
from typing import Any, Optional
from urllib.parse import urlencode
import aiohttp
# pylint: disable=relative-beyond-top-level
from .common import calc_group_id
from .const import (
DEFAULT_OAUTH2_API_HOST,
MIHOME_HTTP_API_TIMEOUT,
OAUTH2_AUTH_URL)
from .miot_error import MIoTErrorCode, MIoTHttpError, MIoTOauthError
_LOGGER = logging.getLogger(__name__)
TOKEN_EXPIRES_TS_RATIO = 0.7
class MIoTOauthClient:
"""oauth agent url, default: product env."""
_main_loop: asyncio.AbstractEventLoop
_session: aiohttp.ClientSession
_oauth_host: str
_client_id: int
_redirect_url: str
_device_id: str
_state: str
def __init__(
self, client_id: str, redirect_url: str, cloud_server: str,
uuid: str, loop: Optional[asyncio.AbstractEventLoop] = None
) -> None:
self._main_loop = loop or asyncio.get_running_loop()
if client_id is None or client_id.strip() == '':
raise MIoTOauthError('invalid client_id')
if not redirect_url:
raise MIoTOauthError('invalid redirect_url')
if not cloud_server:
raise MIoTOauthError('invalid cloud_server')
if not uuid:
raise MIoTOauthError('invalid uuid')
self._client_id = int(client_id)
self._redirect_url = redirect_url
if cloud_server == 'cn':
self._oauth_host = DEFAULT_OAUTH2_API_HOST
else:
self._oauth_host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}'
self._device_id = f'ha.{uuid}'
self._state = hashlib.sha1(
f'd={self._device_id}'.encode('utf-8')).hexdigest()
self._session = aiohttp.ClientSession(loop=self._main_loop)
@property
def state(self) -> str:
return self._state
async def deinit_async(self) -> None:
if self._session and not self._session.closed:
await self._session.close()
def set_redirect_url(self, redirect_url: str) -> None:
if not isinstance(redirect_url, str) or redirect_url.strip() == '':
raise MIoTOauthError('invalid redirect_url')
self._redirect_url = redirect_url
def gen_auth_url(
self,
redirect_url: Optional[str] = None,
state: Optional[str] = None,
scope: Optional[list] = None,
skip_confirm: Optional[bool] = False,
) -> str:
"""get auth url
Args:
redirect_url
state
scope (list, optional):
开放数据接口权限 ID可以传递多个用空格分隔具体值可以参考开放
[数据接口权限列表](https://dev.mi.com/distribute/doc/details?pId=1518).
Defaults to None.\n
skip_confirm (bool, optional):
默认值为true授权有效期内的用户在已登录情况下不显示授权页面直接通过。
如果需要用户每次手动授权设置为false. Defaults to True.\n
Returns:
str: _description_
"""
params: dict = {
'redirect_uri': redirect_url or self._redirect_url,
'client_id': self._client_id,
'response_type': 'code',
'device_id': self._device_id,
'state': self._state
}
if state:
params['state'] = state
if scope:
params['scope'] = ' '.join(scope).strip()
params['skip_confirm'] = skip_confirm
encoded_params = urlencode(params)
return f'{OAUTH2_AUTH_URL}?{encoded_params}'
async def __get_token_async(self, data) -> dict:
http_res = await self._session.get(
url=f'https://{self._oauth_host}/app/v2/ha/oauth/get_token',
params={'data': json.dumps(data)},
headers={'content-type': 'application/x-www-form-urlencoded'},
timeout=MIHOME_HTTP_API_TIMEOUT
)
if http_res.status == 401:
raise MIoTOauthError(
'unauthorized(401)', MIoTErrorCode.CODE_OAUTH_UNAUTHORIZED)
if http_res.status != 200:
raise MIoTOauthError(
f'invalid http status code, {http_res.status}')
res_str = await http_res.text()
res_obj = json.loads(res_str)
if (
not res_obj
or res_obj.get('code', None) != 0
or 'result' not in res_obj
or not all(
key in res_obj['result']
for key in ['access_token', 'refresh_token', 'expires_in'])
):
raise MIoTOauthError(f'invalid http response, {res_str}')
return {
**res_obj['result'],
'expires_ts': int(
time.time() +
(res_obj['result'].get('expires_in', 0)*TOKEN_EXPIRES_TS_RATIO))
}
async def get_access_token_async(self, code: str) -> dict:
"""get access token by authorization code
Args:
code (str): auth code
Returns:
str: _description_
"""
if not isinstance(code, str):
raise MIoTOauthError('invalid code')
return await self.__get_token_async(data={
'client_id': self._client_id,
'redirect_uri': self._redirect_url,
'code': code,
'device_id': self._device_id
})
async def refresh_access_token_async(self, refresh_token: str) -> dict:
"""get access token by refresh token.
Args:
refresh_token (str): refresh_token
Returns:
str: _description_
"""
if not isinstance(refresh_token, str):
raise MIoTOauthError('invalid refresh_token')
return await self.__get_token_async(data={
'client_id': self._client_id,
'redirect_uri': self._redirect_url,
'refresh_token': refresh_token,
})
class MIoTHttpClient:
"""MIoT http client."""
# pylint: disable=inconsistent-quotes
GET_PROP_AGGREGATE_INTERVAL: float = 0.2
GET_PROP_MAX_REQ_COUNT = 150
_main_loop: asyncio.AbstractEventLoop
_session: aiohttp.ClientSession
_host: str
_base_url: str
_client_id: str
_access_token: str
_get_prop_timer: Optional[asyncio.TimerHandle]
_get_prop_list: dict[str, dict]
def __init__(
self, cloud_server: str, client_id: str, access_token: str,
loop: Optional[asyncio.AbstractEventLoop] = None
) -> None:
self._main_loop = loop or asyncio.get_running_loop()
self._host = DEFAULT_OAUTH2_API_HOST
self._base_url = ''
self._client_id = ''
self._access_token = ''
self._get_prop_timer = None
self._get_prop_list = {}
if (
not isinstance(cloud_server, str)
or not isinstance(client_id, str)
or not isinstance(access_token, str)
):
raise MIoTHttpError('invalid params')
self.update_http_header(
cloud_server=cloud_server, client_id=client_id,
access_token=access_token)
self._session = aiohttp.ClientSession(loop=self._main_loop)
async def deinit_async(self) -> None:
if self._get_prop_timer:
self._get_prop_timer.cancel()
self._get_prop_timer = None
for item in self._get_prop_list.values():
fut: Optional[asyncio.Future] = item.get('fut', None)
if fut:
fut.cancel()
self._get_prop_list.clear()
if self._session and not self._session.closed:
await self._session.close()
def update_http_header(
self, cloud_server: Optional[str] = None,
client_id: Optional[str] = None,
access_token: Optional[str] = None
) -> None:
if isinstance(cloud_server, str):
if cloud_server != 'cn':
self._host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}'
self._base_url = f'https://{self._host}'
if isinstance(client_id, str):
self._client_id = client_id
if isinstance(access_token, str):
self._access_token = access_token
@property
def __api_request_headers(self) -> dict:
return {
'Host': self._host,
'X-Client-BizId': 'haapi',
'Content-Type': 'application/json',
'Authorization': f'Bearer{self._access_token}',
'X-Client-AppId': self._client_id,
}
# pylint: disable=unused-private-member
async def __mihome_api_get_async(
self, url_path: str, params: dict,
timeout: int = MIHOME_HTTP_API_TIMEOUT
) -> dict:
http_res = await self._session.get(
url=f'{self._base_url}{url_path}',
params=params,
headers=self.__api_request_headers,
timeout=timeout)
if http_res.status == 401:
raise MIoTHttpError(
'mihome api get failed, unauthorized(401)',
MIoTErrorCode.CODE_HTTP_INVALID_ACCESS_TOKEN)
if http_res.status != 200:
raise MIoTHttpError(
f'mihome api get failed, {http_res.status}, '
f'{url_path}, {params}')
res_str = await http_res.text()
res_obj: dict = json.loads(res_str)
if res_obj.get('code', None) != 0:
raise MIoTHttpError(
f'invalid response code, {res_obj.get("code",None)}, '
f'{res_obj.get("message","")}')
_LOGGER.debug(
'mihome api get, %s%s, %s -> %s',
self._base_url, url_path, params, res_obj)
return res_obj
async def __mihome_api_post_async(
self, url_path: str, data: dict,
timeout: int = MIHOME_HTTP_API_TIMEOUT
) -> dict:
http_res = await self._session.post(
url=f'{self._base_url}{url_path}',
json=data,
headers=self.__api_request_headers,
timeout=timeout)
if http_res.status == 401:
raise MIoTHttpError(
'mihome api get failed, unauthorized(401)',
MIoTErrorCode.CODE_HTTP_INVALID_ACCESS_TOKEN)
if http_res.status != 200:
raise MIoTHttpError(
f'mihome api post failed, {http_res.status}, '
f'{url_path}, {data}')
res_str = await http_res.text()
res_obj: dict = json.loads(res_str)
if res_obj.get('code', None) != 0:
raise MIoTHttpError(
f'invalid response code, {res_obj.get("code",None)}, '
f'{res_obj.get("message","")}')
_LOGGER.debug(
'mihome api post, %s%s, %s -> %s',
self._base_url, url_path, data, res_obj)
return res_obj
async def get_user_info_async(self) -> dict:
http_res = await self._session.get(
url='https://open.account.xiaomi.com/user/profile',
params={
'clientId': self._client_id, 'token': self._access_token},
headers={'content-type': 'application/x-www-form-urlencoded'},
timeout=MIHOME_HTTP_API_TIMEOUT
)
res_str = await http_res.text()
res_obj = json.loads(res_str)
if (
not res_obj
or res_obj.get('code', None) != 0
or 'data' not in res_obj
or 'miliaoNick' not in res_obj['data']
):
raise MIoTOauthError(f'invalid http response, {http_res.text}')
return res_obj['data']
async def get_central_cert_async(self, csr: str) -> str:
if not isinstance(csr, str):
raise MIoTHttpError('invalid params')
res_obj: dict = await self.__mihome_api_post_async(
url_path='/app/v2/ha/oauth/get_central_crt',
data={
'csr': str(base64.b64encode(csr.encode('utf-8')), 'utf-8')
}
)
if 'result' not in res_obj:
raise MIoTHttpError('invalid response result')
cert: str = res_obj['result'].get('cert', None)
if not isinstance(cert, str):
raise MIoTHttpError('invalid cert')
return cert
async def __get_dev_room_page_async(
self, max_id: Optional[str] = None
) -> dict:
res_obj = await self.__mihome_api_post_async(
url_path='/app/v2/homeroom/get_dev_room_page',
data={
'start_id': max_id,
'limit': 150,
},
)
if 'result' not in res_obj and 'info' not in res_obj['result']:
raise MIoTHttpError('invalid response result')
home_list: dict = {}
for home in res_obj['result']['info']:
if 'id' not in home:
_LOGGER.error(
'get dev room page error, invalid home, %s', home)
continue
home_list[str(home['id'])] = {'dids': home.get(
'dids', None) or [], 'room_info': {}}
for room in home.get('roomlist', []):
if 'id' not in room:
_LOGGER.error(
'get dev room page error, invalid room, %s', room)
continue
home_list[str(home['id'])]['room_info'][str(room['id'])] = {
'dids': room.get('dids', None) or []}
if (
res_obj['result'].get('has_more', False)
and isinstance(res_obj['result'].get('max_id', None), str)
):
next_list = await self.__get_dev_room_page_async(
max_id=res_obj['result']['max_id'])
for home_id, info in next_list.items():
home_list.setdefault(home_id, {'dids': [], 'room_info': {}})
home_list[home_id]['dids'].extend(info['dids'])
for room_id, info in info['room_info'].items():
home_list[home_id]['room_info'].setdefault(
room_id, {'dids': []})
home_list[home_id]['room_info'][room_id]['dids'].extend(
info['dids'])
return home_list
async def get_homeinfos_async(self) -> dict:
res_obj = await self.__mihome_api_post_async(
url_path='/app/v2/homeroom/gethome',
data={
'limit': 150,
'fetch_share': True,
'fetch_share_dev': True,
'plat_form': 0,
'app_ver': 9,
},
)
if 'result' not in res_obj:
raise MIoTHttpError('invalid response result')
uid: Optional[str] = None
home_infos: dict = {}
for device_source in ['homelist', 'share_home_list']:
home_infos.setdefault(device_source, {})
for home in res_obj['result'].get(device_source, []):
if (
'id' not in home
or 'name' not in home
or 'roomlist' not in home
):
continue
if uid is None and device_source == 'homelist':
uid = str(home['uid'])
home_infos[device_source][home['id']] = {
'home_id': home['id'],
'home_name': home['name'],
'city_id': home.get('city_id', None),
'longitude': home.get('longitude', None),
'latitude': home.get('latitude', None),
'address': home.get('address', None),
'dids': home.get('dids', []),
'room_info': {
room['id']: {
'room_id': room['id'],
'room_name': room['name'],
'dids': room.get('dids', [])
}
for room in home.get('roomlist', [])
if 'id' in room
},
'group_id': calc_group_id(
uid=home['uid'], home_id=home['id']),
'uid': str(home['uid'])
}
home_infos['uid'] = uid
if (
res_obj['result'].get('has_more', False)
and isinstance(res_obj['result'].get('max_id', None), str)
):
more_list = await self.__get_dev_room_page_async(
max_id=res_obj['result']['max_id'])
for home_id, info in more_list.items():
if home_id not in home_infos['homelist']:
_LOGGER.info('unknown home, %s, %s', home_id, info)
continue
home_infos['homelist'][home_id]['dids'].extend(info['dids'])
for room_id, info in info['room_info'].items():
home_infos['homelist'][home_id]['room_info'].setdefault(
room_id, {
'room_id': room_id,
'room_name': '',
'dids': []})
home_infos['homelist'][home_id]['room_info'][
room_id]['dids'].extend(info['dids'])
return {
'uid': uid,
'home_list': home_infos.get('homelist', {}),
'share_home_list': home_infos.get('share_home_list', [])
}
async def get_uid_async(self) -> str:
return (await self.get_homeinfos_async()).get('uid', None)
async def __get_device_list_page_async(
self, dids: list[str], start_did: Optional[str] = None
) -> dict[str, dict]:
req_data: dict = {
'limit': 200,
'get_split_device': True,
'dids': dids
}
if start_did:
req_data['start_did'] = start_did
device_infos: dict = {}
res_obj = await self.__mihome_api_post_async(
url_path='/app/v2/home/device_list_page',
data=req_data
)
if 'result' not in res_obj:
raise MIoTHttpError('invalid response result')
res_obj = res_obj['result']
for device in res_obj.get('list', []) or []:
did = device.get('did', None)
name = device.get('name', None)
urn = device.get('spec_type', None)
model = device.get('model', None)
if did is None or name is None:
_LOGGER.info(
'invalid device, cloud, %s', device)
continue
if urn is None or model is None:
_LOGGER.info(
'missing the urn|model field, cloud, %s', device)
continue
if did.startswith('miwifi.'):
# The miwifi.* routers defined SPEC functions, but none of them
# were implemented.
_LOGGER.info('ignore miwifi.* device, cloud, %s', did)
continue
device_infos[did] = {
'did': did,
'uid': device.get('uid', None),
'name': name,
'urn': urn,
'model': model,
'connect_type': device.get('pid', -1),
'token': device.get('token', None),
'online': device.get('isOnline', False),
'icon': device.get('icon', None),
'parent_id': device.get('parent_id', None),
'manufacturer': model.split('.')[0],
# 2: xiao-ai, 1: general speaker
'voice_ctrl': device.get('voice_ctrl', 0),
'rssi': device.get('rssi', None),
'owner': device.get('owner', None),
'pid': device.get('pid', None),
'local_ip': device.get('local_ip', None),
'ssid': device.get('ssid', None),
'bssid': device.get('bssid', None),
'order_time': device.get('orderTime', 0),
'fw_version': device.get('extra', {}).get(
'fw_version', 'unknown'),
}
if isinstance(device.get('extra', None), dict) and device['extra']:
device_infos[did]['fw_version'] = device['extra'].get(
'fw_version', None)
device_infos[did]['mcu_version'] = device['extra'].get(
'mcu_version', None)
device_infos[did]['platform'] = device['extra'].get(
'platform', None)
next_start_did = res_obj.get('next_start_did', None)
if res_obj.get('has_more', False) and next_start_did:
device_infos.update(await self.__get_device_list_page_async(
dids=dids, start_did=next_start_did))
return device_infos
async def get_devices_with_dids_async(
self, dids: list[str]
) -> Optional[dict[str, dict]]:
results: list[dict[str, dict]] = await asyncio.gather(
*[self.__get_device_list_page_async(dids=dids[index:index+150])
for index in range(0, len(dids), 150)])
devices = {}
for result in results:
if result is None:
return None
devices.update(result)
return devices
async def get_devices_async(
self, home_ids: Optional[list[str]] = None
) -> dict[str, dict]:
homeinfos = await self.get_homeinfos_async()
homes: dict[str, dict[str, Any]] = {}
devices: dict[str, dict] = {}
for device_type in ['home_list', 'share_home_list']:
homes.setdefault(device_type, {})
for home_id, home_info in (homeinfos.get(
device_type, None) or {}).items():
if isinstance(home_ids, list) and home_id not in home_ids:
continue
home_name: str = home_info['home_name']
group_id: str = home_info['group_id']
homes[device_type].setdefault(
home_id, {
'home_name': home_name,
'uid': home_info['uid'],
'group_id': group_id,
'room_info': {}
})
devices.update({did: {
'home_id': home_id,
'home_name': home_name,
'room_id': home_id,
'room_name': home_name,
'group_id': group_id
} for did in home_info.get('dids', [])})
for room_id, room_info in home_info.get('room_info').items():
room_name: str = room_info.get('room_name', '')
homes[device_type][home_id]['room_info'][
room_id] = room_name
devices.update({
did: {
'home_id': home_id,
'home_name': home_name,
'room_id': room_id,
'room_name': room_name,
'group_id': group_id
} for did in room_info.get('dids', [])})
dids = sorted(list(devices.keys()))
results = await self.get_devices_with_dids_async(dids=dids)
if results is None:
raise MIoTHttpError('get devices failed')
for did in dids:
if did not in results:
devices.pop(did, None)
_LOGGER.info('get device info failed, %s', did)
continue
devices[did].update(results[did])
# Whether sub devices
match_str = re.search(r'\.s\d+$', did)
if not match_str:
continue
device = devices.pop(did, None)
parent_did = did.replace(match_str.group(), '')
if parent_did in devices:
devices[parent_did].setdefault('sub_devices', {})
devices[parent_did]['sub_devices'][match_str.group()[
1:]] = device
else:
_LOGGER.error(
'unknown sub devices, %s, %s', did, parent_did)
return {
'uid': homeinfos['uid'],
'homes': homes,
'devices': devices
}
async def get_props_async(self, params: list) -> list:
"""
params = [{"did": "xxxx", "siid": 2, "piid": 1},
{"did": "xxxxxx", "siid": 2, "piid": 2}]
"""
res_obj = await self.__mihome_api_post_async(
url_path='/app/v2/miotspec/prop/get',
data={
'datasource': 1,
'params': params
},
)
if 'result' not in res_obj:
raise MIoTHttpError('invalid response result')
return res_obj['result']
async def __get_prop_async(self, did: str, siid: int, piid: int) -> Any:
results = await self.get_props_async(
params=[{'did': did, 'siid': siid, 'piid': piid}])
if not results:
return None
result = results[0]
if 'value' not in result:
return None
return result['value']
async def __get_prop_handler(self) -> bool:
props_req: set[str] = set()
props_buffer: list[dict] = []
for key, item in self._get_prop_list.items():
if item.get('tag', False):
continue
# NOTICE: max req prop
if len(props_req) >= self.GET_PROP_MAX_REQ_COUNT:
break
item['tag'] = True
props_buffer.append(item['param'])
props_req.add(key)
if not props_buffer:
_LOGGER.error('get prop error, empty request list')
return False
results = await self.get_props_async(props_buffer)
for result in results:
if not all(
key in result for key in ['did', 'siid', 'piid', 'value']):
continue
key = f'{result["did"]}.{result["siid"]}.{result["piid"]}'
prop_obj = self._get_prop_list.pop(key, None)
if prop_obj is None:
_LOGGER.info('get prop error, key not exists, %s', result)
continue
prop_obj['fut'].set_result(result['value'])
props_req.remove(key)
for key in props_req:
prop_obj = self._get_prop_list.pop(key, None)
if prop_obj is None:
continue
prop_obj['fut'].set_result(None)
if props_req:
_LOGGER.info(
'get prop from cloud failed, %s', props_req)
if self._get_prop_list:
self._get_prop_timer = self._main_loop.call_later(
self.GET_PROP_AGGREGATE_INTERVAL,
lambda: self._main_loop.create_task(
self.__get_prop_handler()))
else:
self._get_prop_timer = None
return True
async def get_prop_async(
self, did: str, siid: int, piid: int, immediately: bool = False
) -> Any:
if immediately:
return await self.__get_prop_async(did, siid, piid)
key: str = f'{did}.{siid}.{piid}'
prop_obj = self._get_prop_list.get(key, None)
if prop_obj:
return await prop_obj['fut']
fut = self._main_loop.create_future()
self._get_prop_list[key] = {
'param': {'did': did, 'siid': siid, 'piid': piid},
'fut': fut
}
if self._get_prop_timer is None:
self._get_prop_timer = self._main_loop.call_later(
self.GET_PROP_AGGREGATE_INTERVAL,
lambda: self._main_loop.create_task(
self.__get_prop_handler()))
return await fut
async def set_prop_async(self, params: list) -> list:
"""
params = [{"did": "xxxx", "siid": 2, "piid": 1, "value": False}]
"""
res_obj = await self.__mihome_api_post_async(
url_path='/app/v2/miotspec/prop/set',
data={
'params': params
},
timeout=15
)
if 'result' not in res_obj:
raise MIoTHttpError('invalid response result')
return res_obj['result']
async def action_async(
self, did: str, siid: int, aiid: int, in_list: list[dict]
) -> dict:
"""
params = {"did": "xxxx", "siid": 2, "aiid": 1, "in": []}
"""
# NOTICE: Non-standard action param
res_obj = await self.__mihome_api_post_async(
url_path='/app/v2/miotspec/action',
data={
'params': {
'did': did,
'siid': siid,
'aiid': aiid,
'in': [item['value'] for item in in_list]}
},
timeout=15
)
if 'result' not in res_obj:
raise MIoTHttpError('invalid response result')
return res_obj['result']