mirror of
https://github.com/XiaoMi/ha_xiaomi_home.git
synced 2025-07-06 11:49:05 +08:00
Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
67785f747a | |||
bd3a98b976 | |||
6ce3206b30 | |||
aacb794e1f | |||
a879ae2cdf | |||
640ac25d9c | |||
5f5b3feea5 | |||
571483b302 | |||
b955c199fc | |||
f10885fbfd | |||
bba8ba7f7b | |||
1f20416e97 | |||
2d6387c30a | |||
b93d8631b8 | |||
dabf277942 | |||
c4a981a15d | |||
3c464a0b0c | |||
b4be1e0aa9 | |||
8364a544f2 | |||
d980b1bfb4 | |||
2fb030c52d | |||
54e26378be | |||
6fae26a378 | |||
01f6bbf2c7 |
16
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
16
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -1,11 +1,9 @@
|
||||
name: Bug report / 报告问题
|
||||
description: Create a report to help us improve. / 报告问题以帮助我们改进
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Describe the bug / 描述问题
|
||||
label: Describe the Bug / 描述问题
|
||||
description: |
|
||||
> A clear and concise description of what the bug is.
|
||||
> 清晰且简明地描述问题。
|
||||
@ -14,7 +12,7 @@ body:
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To Reproduce / 复现步骤
|
||||
label: How to Reproduce / 复现步骤
|
||||
description: |
|
||||
> If applicable, add screenshots to help explain your problem. You can attach images by clicking this area to highlight it and then dragging files in. Steps to reproduce the behavior:
|
||||
> 如有需要,可添加截图以帮助解释问题。点击此区域以高亮显示并拖动截图文件以上传。请详细描述复现步骤:
|
||||
@ -28,7 +26,7 @@ body:
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Expected behavior / 预期结果
|
||||
label: Expected Behavior / 预期结果
|
||||
description: |
|
||||
> A clear and concise description of what you expected to happen.
|
||||
> 描述预期结果。
|
||||
@ -44,7 +42,7 @@ body:
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Home Assistant Core version / Home Assistant Core 版本
|
||||
label: Home Assistant Core Version / Home Assistant Core 版本
|
||||
description: |
|
||||
> [Settings > About](https://my.home-assistant.io/redirect/info)
|
||||
> [设置 > 关于 Home Assistant](https://my.home-assistant.io/redirect/info)
|
||||
@ -54,7 +52,7 @@ body:
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Home Assistant Operation System version / Home Assistant Operation System 版本
|
||||
label: Home Assistant Operation System Version / Home Assistant Operation System 版本
|
||||
description: |
|
||||
> [Settings > About](https://my.home-assistant.io/redirect/info)
|
||||
> [设置 > 关于 Home Assistant](https://my.home-assistant.io/redirect/info)
|
||||
@ -64,7 +62,7 @@ body:
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Xiaomi Home integration version / 米家集成版本
|
||||
label: Xiaomi Home Integration Version / 米家集成版本
|
||||
description: |
|
||||
> [Settings > Devices & services > Configured > `Xiaomi Home`](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
|
||||
> [设置 > 设备与服务 > 已配置 > `Xiaomi Home`](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
|
||||
@ -74,4 +72,4 @@ body:
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context / 其他说明
|
||||
label: Additional Context / 其他说明
|
||||
|
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,5 +1,16 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.1.3
|
||||
### Added
|
||||
### Changed
|
||||
- Remove default bug label. [#276](https://github.com/XiaoMi/ha_xiaomi_home/pull/276)
|
||||
- Improve multi-language translation actions. [#256](https://github.com/XiaoMi/ha_xiaomi_home/pull/256)
|
||||
- Use aiohttp instead of waiting for blocking calls. [#227](https://github.com/XiaoMi/ha_xiaomi_home/pull/227)
|
||||
- Language supports dt. [#237](https://github.com/XiaoMi/ha_xiaomi_home/pull/237)
|
||||
### Fixed
|
||||
- Fix local control error. [#271](https://github.com/XiaoMi/ha_xiaomi_home/pull/271)
|
||||
- Fix README_zh and miot_storage. [#270](https://github.com/XiaoMi/ha_xiaomi_home/pull/270)
|
||||
|
||||
## v0.1.2
|
||||
### Added
|
||||
- Support Xiaomi Heater devices. https://github.com/XiaoMi/ha_xiaomi_home/issues/124 https://github.com/XiaoMi/ha_xiaomi_home/issues/117
|
||||
|
@ -8,7 +8,7 @@ Xiaomi Home Integration is an integrated component of Home Assistant supported b
|
||||
|
||||
> Home Assistant version requirement:
|
||||
>
|
||||
> - Core $\geq$ 2024.11.0
|
||||
> - Core $\geq$ 2024.4.4
|
||||
> - Operating System $\geq$ 13.0
|
||||
|
||||
### Method 1: Git clone from GitHub
|
||||
|
@ -23,10 +23,11 @@
|
||||
"paho-mqtt<=2.0.0",
|
||||
"numpy",
|
||||
"cryptography",
|
||||
"psutil"
|
||||
"psutil",
|
||||
"aiohttp[speedups]"
|
||||
],
|
||||
"version": "v0.1.2",
|
||||
"version": "v0.1.3",
|
||||
"zeroconf": [
|
||||
"_miot-central._tcp.local."
|
||||
]
|
||||
}
|
||||
}
|
@ -46,10 +46,19 @@ off Xiaomi or its affiliates' products.
|
||||
Common utilities.
|
||||
"""
|
||||
import json
|
||||
from os import path
|
||||
import random
|
||||
from typing import Optional
|
||||
import hashlib
|
||||
from paho.mqtt.client import MQTTMatcher
|
||||
import yaml
|
||||
|
||||
MIOT_ROOT_PATH: str = path.dirname(path.abspath(__file__))
|
||||
|
||||
|
||||
def gen_absolute_path(relative_path: str) -> str:
|
||||
"""Generate an absolute path."""
|
||||
return path.join(MIOT_ROOT_PATH, relative_path)
|
||||
|
||||
|
||||
def calc_group_id(uid: str, home_id: str) -> str:
|
||||
@ -64,6 +73,12 @@ def load_json_file(json_file: str) -> dict:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_yaml_file(yaml_file: str) -> dict:
|
||||
"""Load a YAML file."""
|
||||
with open(yaml_file, 'r', encoding='utf-8') as f:
|
||||
return yaml.load(f, Loader=yaml.FullLoader)
|
||||
|
||||
|
||||
def randomize_int(value: int, ratio: float) -> int:
|
||||
"""Randomize an integer value."""
|
||||
return int(value * (1 - ratio + random.random()*2*ratio))
|
||||
@ -74,12 +89,12 @@ class MIoTMatcher(MQTTMatcher):
|
||||
|
||||
def iter_all_nodes(self) -> any:
|
||||
"""Return an iterator on all nodes with their paths and contents."""
|
||||
def rec(node, path):
|
||||
def rec(node, path_):
|
||||
# pylint: disable=protected-access
|
||||
if node._content:
|
||||
yield ('/'.join(path), node._content)
|
||||
yield ('/'.join(path_), node._content)
|
||||
for part, child in node._children.items():
|
||||
yield from rec(child, path + [part])
|
||||
yield from rec(child, path_ + [part])
|
||||
return rec(self._root, [])
|
||||
|
||||
def get(self, topic: str) -> Optional[any]:
|
||||
|
@ -67,24 +67,16 @@ SPEC_STD_LIB_EFFECTIVE_TIME = 3600*24*14
|
||||
MANUFACTURER_EFFECTIVE_TIME = 3600*24*14
|
||||
|
||||
SUPPORTED_PLATFORMS: list = [
|
||||
# 'alarm_control_panel',
|
||||
'binary_sensor',
|
||||
'button',
|
||||
'climate',
|
||||
# 'camera',
|
||||
# 'conversation',
|
||||
'cover',
|
||||
# 'device_tracker',
|
||||
'event',
|
||||
'fan',
|
||||
'humidifier',
|
||||
'light',
|
||||
# 'lock',
|
||||
# 'media_player',
|
||||
'notify',
|
||||
'number',
|
||||
# 'remote',
|
||||
# 'scene',
|
||||
'select',
|
||||
'sensor',
|
||||
'switch',
|
||||
@ -107,16 +99,17 @@ SUPPORT_CENTRAL_GATEWAY_CTRL: list = ['cn']
|
||||
|
||||
DEFAULT_INTEGRATION_LANGUAGE: str = 'en'
|
||||
INTEGRATION_LANGUAGES = {
|
||||
'zh-Hans': '简体中文',
|
||||
'zh-Hant': '繁體中文',
|
||||
'en': 'English',
|
||||
'de': 'Deutsch',
|
||||
'en': 'English',
|
||||
'es': 'Español',
|
||||
'fr': 'Français',
|
||||
'ja': '日本語',
|
||||
'nl': 'Nederlands',
|
||||
'pt': 'Português',
|
||||
'pt-BR': 'Português (Brasil)',
|
||||
'ru': 'Русский',
|
||||
'zh-Hans': '简体中文',
|
||||
'zh-Hant': '繁體中文'
|
||||
}
|
||||
|
||||
DEFAULT_CTRL_MODE: str = 'auto'
|
||||
|
95
custom_components/xiaomi_home/miot/i18n/nl.json
Normal file
95
custom_components/xiaomi_home/miot/i18n/nl.json
Normal file
@ -0,0 +1,95 @@
|
||||
{
|
||||
"config": {
|
||||
"other": {
|
||||
"devices": "Apparaten",
|
||||
"found_central_gateway": ", Lokale centrale hub-gateway gevonden"
|
||||
},
|
||||
"control_mode": {
|
||||
"auto": "Automatisch",
|
||||
"cloud": "Cloud"
|
||||
},
|
||||
"room_name_rule": {
|
||||
"none": "Niet synchroniseren",
|
||||
"home_room": "Huisnaam en Kamernaam (Xiaomi Home Slaapkamer)",
|
||||
"room": "Kamernaam (Slaapkamer)",
|
||||
"home": "Huisnaam (Xiaomi Home)"
|
||||
},
|
||||
"option_status": {
|
||||
"enable": "Inschakelen",
|
||||
"disable": "Uitschakelen"
|
||||
},
|
||||
"lan_ctrl_config": {
|
||||
"notice_net_dup": "\r\n**[Let op]** Meerdere netwerkkaarten gedetecteerd die mogelijk zijn verbonden met hetzelfde netwerk. Let op bij de selectie.",
|
||||
"net_unavailable": "Interface niet beschikbaar"
|
||||
}
|
||||
},
|
||||
"miot": {
|
||||
"client": {
|
||||
"invalid_oauth_info": "Authenticatie-informatie is ongeldig, cloudverbinding zal niet beschikbaar zijn. Ga naar de Xiaomi Home-integratiepagina en klik op 'Opties' om opnieuw te verifiëren.",
|
||||
"invalid_device_cache": "Cache van apparaatgegevens is abnormaal. Ga naar de Xiaomi Home-integratiepagina, klik op 'Opties->Apparaatlijst bijwerken' en werk de lokale cache bij.",
|
||||
"invalid_cert_info": "Ongeldig gebruikerscertificaat, lokale centrale verbinding zal niet beschikbaar zijn. Ga naar de Xiaomi Home-integratiepagina en klik op 'Opties' om opnieuw te verifiëren.",
|
||||
"device_cloud_error": "Er is een uitzondering opgetreden bij het ophalen van apparaatgegevens uit de cloud. Controleer de lokale netwerkverbinding.",
|
||||
"xiaomi_home_error_title": "Xiaomi Home-integratiefout",
|
||||
"xiaomi_home_error": "Gedetecteerd **{nick_name}({uid}, {cloud_server})** fout, ga naar de optiespagina om opnieuw te configureren.\n\n**Foutmelding**: \n{message}",
|
||||
"device_list_changed_title": "Wijzigingen in Xiaomi Home-apparaatlijst",
|
||||
"device_list_changed": "Gedetecteerd **{nick_name}({uid}, {cloud_server})** apparaatgegevens zijn gewijzigd. Ga naar de integratie-optiespagina, klik op `Opties->Apparaatlijst bijwerken` en werk lokale apparaatgegevens bij.\n\nHuidige netwerkstatus: {network_status}\n{message}\n",
|
||||
"device_list_add": "\n**{count} nieuwe apparaten:** \n{message}",
|
||||
"device_list_del": "\n**{count} apparaten niet beschikbaar:** \n{message}",
|
||||
"device_list_offline": "\n**{count} apparaten offline:** \n{message}",
|
||||
"network_status_online": "Online",
|
||||
"network_status_offline": "Offline",
|
||||
"device_exec_error": "Uitvoeringsfout"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"common": {
|
||||
"-10000": "Onbekende fout",
|
||||
"-10001": "Service niet beschikbaar",
|
||||
"-10002": "Ongeldige parameter",
|
||||
"-10003": "Onvoldoende middelen",
|
||||
"-10004": "Interne fout",
|
||||
"-10005": "Onvoldoende machtigingen",
|
||||
"-10006": "Uitvoeringstijd verstreken",
|
||||
"-10007": "Apparaat offline of bestaat niet",
|
||||
"-10020": "Niet geautoriseerd (OAuth2)",
|
||||
"-10030": "Ongeldig token (HTTP)",
|
||||
"-10040": "Ongeldig berichtformaat",
|
||||
"-10050": "Ongeldig certificaat",
|
||||
"-704000000": "Onbekende fout",
|
||||
"-704010000": "Niet geautoriseerd (apparaat kan zijn verwijderd)",
|
||||
"-704014006": "Apparaatbeschrijving niet gevonden",
|
||||
"-704030013": "Eigenschap niet leesbaar",
|
||||
"-704030023": "Eigenschap niet schrijfbaar",
|
||||
"-704030033": "Eigenschap niet abonneerbaar",
|
||||
"-704040002": "Service bestaat niet",
|
||||
"-704040003": "Eigenschap bestaat niet",
|
||||
"-704040004": "Gebeurtenis bestaat niet",
|
||||
"-704040005": "Actie bestaat niet",
|
||||
"-704040999": "Functie niet online",
|
||||
"-704042001": "Apparaat bestaat niet",
|
||||
"-704042011": "Apparaat offline",
|
||||
"-704053036": "Apparaatbedieningstijd verstreken",
|
||||
"-704053100": "Apparaat kan deze handeling niet uitvoeren in de huidige staat",
|
||||
"-704083036": "Apparaatbedieningstijd verstreken",
|
||||
"-704090001": "Apparaat bestaat niet",
|
||||
"-704220008": "Ongeldige ID",
|
||||
"-704220025": "Aantal actieparameters komt niet overeen",
|
||||
"-704220035": "Fout in actieparameter",
|
||||
"-704220043": "Fout in eigenschapswaarde",
|
||||
"-704222034": "Fout in retourwaarde actie",
|
||||
"-705004000": "Onbekende fout",
|
||||
"-705004501": "Onbekende fout",
|
||||
"-705201013": "Eigenschap niet leesbaar",
|
||||
"-705201015": "Fout bij uitvoeren van actie",
|
||||
"-705201023": "Eigenschap niet schrijfbaar",
|
||||
"-705201033": "Eigenschap niet abonneerbaar",
|
||||
"-706012000": "Onbekende fout",
|
||||
"-706012013": "Eigenschap niet leesbaar",
|
||||
"-706012015": "Fout bij uitvoeren van actie",
|
||||
"-706012023": "Eigenschap niet schrijfbaar",
|
||||
"-706012033": "Eigenschap niet abonneerbaar",
|
||||
"-706012043": "Fout in eigenschapswaarde",
|
||||
"-706014006": "Apparaatbeschrijving niet gevonden"
|
||||
}
|
||||
}
|
||||
}
|
@ -43,20 +43,18 @@
|
||||
},
|
||||
"error": {
|
||||
"common": {
|
||||
"-1": "未知錯誤",
|
||||
"-10000": "未知錯誤",
|
||||
"-10001": "服務不可用",
|
||||
"-10002": "無效參數",
|
||||
"-10002": "參數無效",
|
||||
"-10003": "資源不足",
|
||||
"-10004": "內部錯誤",
|
||||
"-10005": "權限不足",
|
||||
"-10006": "執行超時",
|
||||
"-10007": "設備離線或者不存在",
|
||||
"-10020": "無效的消息格式"
|
||||
},
|
||||
"gw": {},
|
||||
"lan": {},
|
||||
"cloud": {
|
||||
"-10020": "未授權(OAuth2)",
|
||||
"-10030": "無效的token(HTTP)",
|
||||
"-10040": "無效的消息格式",
|
||||
"-10050": "無效的證書",
|
||||
"-704000000": "未知錯誤",
|
||||
"-704010000": "未授權(設備可能被刪除)",
|
||||
"-704014006": "沒找到設備描述",
|
||||
|
1320
custom_components/xiaomi_home/miot/lan/profile_models.yaml
Normal file
1320
custom_components/xiaomi_home/miot/lan/profile_models.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@ -51,10 +51,9 @@ import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from functools import partial
|
||||
from typing import Optional
|
||||
from urllib.parse import urlencode
|
||||
import requests
|
||||
import aiohttp
|
||||
|
||||
# pylint: disable=relative-beyond-top-level
|
||||
from .common import calc_group_id
|
||||
@ -71,8 +70,9 @@ TOKEN_EXPIRES_TS_RATIO = 0.7
|
||||
|
||||
class MIoTOauthClient:
|
||||
"""oauth agent url, default: product env."""
|
||||
_main_loop: asyncio.AbstractEventLoop = None
|
||||
_oauth_host: str = None
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
_session: aiohttp.ClientSession
|
||||
_oauth_host: str
|
||||
_client_id: int
|
||||
_redirect_url: str
|
||||
|
||||
@ -94,9 +94,10 @@ class MIoTOauthClient:
|
||||
self._oauth_host = DEFAULT_OAUTH2_API_HOST
|
||||
else:
|
||||
self._oauth_host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}'
|
||||
self._session = aiohttp.ClientSession()
|
||||
|
||||
async def __call_async(self, func):
|
||||
return await self._main_loop.run_in_executor(executor=None, func=func)
|
||||
def __del__(self):
|
||||
self._session.close()
|
||||
|
||||
def set_redirect_url(self, redirect_url: str) -> None:
|
||||
if not isinstance(redirect_url, str) or redirect_url.strip() == '':
|
||||
@ -140,21 +141,22 @@ class MIoTOauthClient:
|
||||
|
||||
return f'{OAUTH2_AUTH_URL}?{encoded_params}'
|
||||
|
||||
def _get_token(self, data) -> dict:
|
||||
http_res = requests.get(
|
||||
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_code == 401:
|
||||
if http_res.status == 401:
|
||||
raise MIoTOauthError(
|
||||
'unauthorized(401)', MIoTErrorCode.CODE_OAUTH_UNAUTHORIZED)
|
||||
if http_res.status_code != 200:
|
||||
if http_res.status != 200:
|
||||
raise MIoTOauthError(
|
||||
f'invalid http status code, {http_res.status_code}')
|
||||
f'invalid http status code, {http_res.status}')
|
||||
|
||||
res_obj = http_res.json()
|
||||
res_str = await http_res.text()
|
||||
res_obj = json.loads(res_str)
|
||||
if (
|
||||
not res_obj
|
||||
or res_obj.get('code', None) != 0
|
||||
@ -172,7 +174,7 @@ class MIoTOauthClient:
|
||||
(res_obj['result'].get('expires_in', 0)*TOKEN_EXPIRES_TS_RATIO))
|
||||
}
|
||||
|
||||
def get_access_token(self, code: str) -> dict:
|
||||
async def get_access_token_async(self, code: str) -> dict:
|
||||
"""get access token by authorization code
|
||||
|
||||
Args:
|
||||
@ -184,16 +186,13 @@ class MIoTOauthClient:
|
||||
if not isinstance(code, str):
|
||||
raise MIoTOauthError('invalid code')
|
||||
|
||||
return self._get_token(data={
|
||||
return await self.__get_token_async(data={
|
||||
'client_id': self._client_id,
|
||||
'redirect_uri': self._redirect_url,
|
||||
'code': code,
|
||||
})
|
||||
|
||||
async def get_access_token_async(self, code: str) -> dict:
|
||||
return await self.__call_async(partial(self.get_access_token, code))
|
||||
|
||||
def refresh_access_token(self, refresh_token: str) -> dict:
|
||||
async def refresh_access_token_async(self, refresh_token: str) -> dict:
|
||||
"""get access token by refresh token.
|
||||
|
||||
Args:
|
||||
@ -205,16 +204,12 @@ class MIoTOauthClient:
|
||||
if not isinstance(refresh_token, str):
|
||||
raise MIoTOauthError('invalid refresh_token')
|
||||
|
||||
return self._get_token(data={
|
||||
return await self._get_token_async(data={
|
||||
'client_id': self._client_id,
|
||||
'redirect_uri': self._redirect_url,
|
||||
'refresh_token': refresh_token,
|
||||
})
|
||||
|
||||
async def refresh_access_token_async(self, refresh_token: str) -> dict:
|
||||
return await self.__call_async(
|
||||
partial(self.refresh_access_token, refresh_token))
|
||||
|
||||
|
||||
class MIoTHttpClient:
|
||||
"""MIoT http client."""
|
||||
@ -222,6 +217,7 @@ class MIoTHttpClient:
|
||||
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
|
||||
@ -254,10 +250,10 @@ class MIoTHttpClient:
|
||||
cloud_server=cloud_server, client_id=client_id,
|
||||
access_token=access_token)
|
||||
|
||||
async def __call_async(self, func) -> any:
|
||||
if self._main_loop is None:
|
||||
raise MIoTHttpError('miot http, un-support async methods')
|
||||
return await self._main_loop.run_in_executor(executor=None, func=func)
|
||||
self._session = aiohttp.ClientSession()
|
||||
|
||||
def __del__(self):
|
||||
self._session.close()
|
||||
|
||||
def update_http_header(
|
||||
self, cloud_server: Optional[str] = None,
|
||||
@ -276,36 +272,35 @@ class MIoTHttpClient:
|
||||
self._access_token = access_token
|
||||
|
||||
@property
|
||||
def __api_session(self) -> requests.Session:
|
||||
session = requests.Session()
|
||||
session.headers.update({
|
||||
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,
|
||||
})
|
||||
return session
|
||||
}
|
||||
|
||||
def mihome_api_get(
|
||||
# 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 = None
|
||||
with self.__api_session as session:
|
||||
http_res = session.get(
|
||||
url=f'{self._base_url}{url_path}',
|
||||
params=params,
|
||||
timeout=timeout)
|
||||
if http_res.status_code == 401:
|
||||
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_code != 200:
|
||||
if http_res.status != 200:
|
||||
raise MIoTHttpError(
|
||||
f'mihome api get failed, {http_res.status_code}, '
|
||||
f'mihome api get failed, {http_res.status}, '
|
||||
f'{url_path}, {params}')
|
||||
res_obj: dict = http_res.json()
|
||||
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)}, '
|
||||
@ -315,28 +310,25 @@ class MIoTHttpClient:
|
||||
self._base_url, url_path, params, res_obj)
|
||||
return res_obj
|
||||
|
||||
def mihome_api_post(
|
||||
async def __mihome_api_post_async(
|
||||
self, url_path: str, data: dict,
|
||||
timeout: int = MIHOME_HTTP_API_TIMEOUT
|
||||
) -> dict:
|
||||
encoded_data = None
|
||||
if data:
|
||||
encoded_data = json.dumps(data).encode('utf-8')
|
||||
http_res = None
|
||||
with self.__api_session as session:
|
||||
http_res = session.post(
|
||||
url=f'{self._base_url}{url_path}',
|
||||
data=encoded_data,
|
||||
timeout=timeout)
|
||||
if http_res.status_code == 401:
|
||||
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_code != 200:
|
||||
if http_res.status != 200:
|
||||
raise MIoTHttpError(
|
||||
f'mihome api post failed, {http_res.status_code}, '
|
||||
f'mihome api post failed, {http_res.status}, '
|
||||
f'{url_path}, {data}')
|
||||
res_obj: dict = http_res.json()
|
||||
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)}, '
|
||||
@ -346,8 +338,8 @@ class MIoTHttpClient:
|
||||
self._base_url, url_path, data, res_obj)
|
||||
return res_obj
|
||||
|
||||
def get_user_info(self) -> dict:
|
||||
http_res = requests.get(
|
||||
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},
|
||||
@ -355,7 +347,8 @@ class MIoTHttpClient:
|
||||
timeout=MIHOME_HTTP_API_TIMEOUT
|
||||
)
|
||||
|
||||
res_obj = http_res.json()
|
||||
res_str = await http_res.text()
|
||||
res_obj = json.loads(res_str)
|
||||
if (
|
||||
not res_obj
|
||||
or res_obj.get('code', None) != 0
|
||||
@ -366,14 +359,11 @@ class MIoTHttpClient:
|
||||
|
||||
return res_obj['data']
|
||||
|
||||
async def get_user_info_async(self) -> dict:
|
||||
return await self.__call_async(partial(self.get_user_info))
|
||||
|
||||
def get_central_cert(self, csr: str) -> Optional[str]:
|
||||
async def get_central_cert_async(self, csr: str) -> Optional[str]:
|
||||
if not isinstance(csr, str):
|
||||
raise MIoTHttpError('invalid params')
|
||||
|
||||
res_obj: dict = self.mihome_api_post(
|
||||
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')
|
||||
@ -387,11 +377,8 @@ class MIoTHttpClient:
|
||||
|
||||
return cert
|
||||
|
||||
async def get_central_cert_async(self, csr: str) -> Optional[str]:
|
||||
return await self.__call_async(partial(self.get_central_cert, csr))
|
||||
|
||||
def __get_dev_room_page(self, max_id: str = None) -> dict:
|
||||
res_obj = self.mihome_api_post(
|
||||
async def __get_dev_room_page_async(self, max_id: 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,
|
||||
@ -419,7 +406,7 @@ class MIoTHttpClient:
|
||||
res_obj['result'].get('has_more', False)
|
||||
and isinstance(res_obj['result'].get('max_id', None), str)
|
||||
):
|
||||
next_list = self.__get_dev_room_page(
|
||||
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': {}})
|
||||
@ -432,8 +419,8 @@ class MIoTHttpClient:
|
||||
|
||||
return home_list
|
||||
|
||||
def get_homeinfos(self) -> dict:
|
||||
res_obj = self.mihome_api_post(
|
||||
async def get_homeinfos_async(self) -> dict:
|
||||
res_obj = await self.__mihome_api_post_async(
|
||||
url_path='/app/v2/homeroom/gethome',
|
||||
data={
|
||||
'limit': 150,
|
||||
@ -485,7 +472,7 @@ class MIoTHttpClient:
|
||||
res_obj['result'].get('has_more', False)
|
||||
and isinstance(res_obj['result'].get('max_id', None), str)
|
||||
):
|
||||
more_list = self.__get_dev_room_page(
|
||||
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']:
|
||||
@ -507,16 +494,10 @@ class MIoTHttpClient:
|
||||
'share_home_list': home_infos.get('share_home_list', [])
|
||||
}
|
||||
|
||||
async def get_homeinfos_async(self) -> dict:
|
||||
return await self.__call_async(self.get_homeinfos)
|
||||
|
||||
def get_uid(self) -> str:
|
||||
return self.get_homeinfos().get('uid', None)
|
||||
|
||||
async def get_uid_async(self) -> str:
|
||||
return (await self.get_homeinfos_async()).get('uid', None)
|
||||
|
||||
def __get_device_list_page(
|
||||
async def __get_device_list_page_async(
|
||||
self, dids: list[str], start_did: str = None
|
||||
) -> dict[str, dict]:
|
||||
req_data: dict = {
|
||||
@ -527,7 +508,7 @@ class MIoTHttpClient:
|
||||
if start_did:
|
||||
req_data['start_did'] = start_did
|
||||
device_infos: dict = {}
|
||||
res_obj = self.mihome_api_post(
|
||||
res_obj = await self.__mihome_api_post_async(
|
||||
url_path='/app/v2/home/device_list_page',
|
||||
data=req_data
|
||||
)
|
||||
@ -578,7 +559,7 @@ class MIoTHttpClient:
|
||||
|
||||
next_start_did = res_obj.get('next_start_did', None)
|
||||
if res_obj.get('has_more', False) and next_start_did:
|
||||
device_infos.update(self.__get_device_list_page(
|
||||
device_infos.update(await self.__get_device_list_page_async(
|
||||
dids=dids, start_did=next_start_did))
|
||||
|
||||
return device_infos
|
||||
@ -587,8 +568,7 @@ class MIoTHttpClient:
|
||||
self, dids: list[str]
|
||||
) -> dict[str, dict]:
|
||||
results: list[dict[str, dict]] = await asyncio.gather(
|
||||
*[self.__call_async(
|
||||
partial(self.__get_device_list_page, dids[index:index+150]))
|
||||
*[self.__get_device_list_page_async(dids[index:index+150])
|
||||
for index in range(0, len(dids), 150)])
|
||||
devices = {}
|
||||
for result in results:
|
||||
@ -665,12 +645,12 @@ class MIoTHttpClient:
|
||||
'devices': devices
|
||||
}
|
||||
|
||||
def get_props(self, params: list) -> list:
|
||||
async def get_props_async(self, params: list) -> list:
|
||||
"""
|
||||
params = [{"did": "xxxx", "siid": 2, "piid": 1},
|
||||
{"did": "xxxxxx", "siid": 2, "piid": 2}]
|
||||
"""
|
||||
res_obj = self.mihome_api_post(
|
||||
res_obj = await self.__mihome_api_post_async(
|
||||
url_path='/app/v2/miotspec/prop/get',
|
||||
data={
|
||||
'datasource': 1,
|
||||
@ -681,11 +661,9 @@ class MIoTHttpClient:
|
||||
raise MIoTHttpError('invalid response result')
|
||||
return res_obj['result']
|
||||
|
||||
async def get_props_async(self, params: list) -> list:
|
||||
return await self.__call_async(partial(self.get_props, params))
|
||||
|
||||
def get_prop(self, did: str, siid: int, piid: int) -> any:
|
||||
results = self.get_props(
|
||||
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
|
||||
@ -711,7 +689,7 @@ class MIoTHttpClient:
|
||||
if not props_buffer:
|
||||
_LOGGER.error('get prop error, empty request list')
|
||||
return False
|
||||
results = await self.__call_async(partial(self.get_props, props_buffer))
|
||||
results = await self.get_props_async(props_buffer)
|
||||
|
||||
for result in results:
|
||||
if not all(
|
||||
@ -747,8 +725,7 @@ class MIoTHttpClient:
|
||||
self, did: str, siid: int, piid: int, immediately: bool = False
|
||||
) -> any:
|
||||
if immediately:
|
||||
return await self.__call_async(
|
||||
partial(self.get_prop, did, siid, piid))
|
||||
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:
|
||||
@ -766,11 +743,11 @@ class MIoTHttpClient:
|
||||
|
||||
return await fut
|
||||
|
||||
def set_prop(self, params: list) -> list:
|
||||
async def set_prop_async(self, params: list) -> list:
|
||||
"""
|
||||
params = [{"did": "xxxx", "siid": 2, "piid": 1, "value": False}]
|
||||
"""
|
||||
res_obj = self.mihome_api_post(
|
||||
res_obj = await self.__mihome_api_post_async(
|
||||
url_path='/app/v2/miotspec/prop/set',
|
||||
data={
|
||||
'params': params
|
||||
@ -782,20 +759,14 @@ class MIoTHttpClient:
|
||||
|
||||
return res_obj['result']
|
||||
|
||||
async def set_prop_async(self, params: list) -> list:
|
||||
"""
|
||||
params = [{"did": "xxxx", "siid": 2, "piid": 1, "value": False}]
|
||||
"""
|
||||
return await self.__call_async(partial(self.set_prop, params))
|
||||
|
||||
def action(
|
||||
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 = self.mihome_api_post(
|
||||
res_obj = await self.__mihome_api_post_async(
|
||||
url_path='/app/v2/miotspec/action',
|
||||
data={
|
||||
'params': {
|
||||
@ -810,9 +781,3 @@ class MIoTHttpClient:
|
||||
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:
|
||||
return await self.__call_async(
|
||||
partial(self.action, did, siid, aiid, in_list))
|
||||
|
@ -71,7 +71,8 @@ from .miot_error import MIoTErrorCode
|
||||
from .miot_ev import MIoTEventLoop, TimeoutHandle
|
||||
from .miot_network import InterfaceStatus, MIoTNetwork, NetworkInfo
|
||||
from .miot_mdns import MipsService, MipsServiceState
|
||||
from .common import randomize_int, MIoTMatcher
|
||||
from .common import (
|
||||
randomize_int, load_yaml_file, gen_absolute_path, MIoTMatcher)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -175,7 +176,7 @@ class MIoTLanDevice:
|
||||
OT_HEADER_LEN: int = 32
|
||||
NETWORK_UNSTABLE_CNT_TH: int = 10
|
||||
NETWORK_UNSTABLE_TIME_TH: int = 120000
|
||||
NETWORK_UNSTABLE_RESUME_TH: int = 300
|
||||
NETWORK_UNSTABLE_RESUME_TH: int = 300000
|
||||
FAST_PING_INTERVAL: int = 5000
|
||||
CONSTRUCT_STATE_PENDING: int = 15000
|
||||
KA_INTERVAL_MIN = 10000
|
||||
@ -472,6 +473,8 @@ class MIoTLan:
|
||||
OT_PROBE_INTERVAL_MIN: int = 5000
|
||||
OT_PROBE_INTERVAL_MAX: int = 45000
|
||||
|
||||
PROFILE_MODELS_FILE: str = 'lan/profile_models.yaml'
|
||||
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
_net_ifs: set[str]
|
||||
_network: MIoTNetwork
|
||||
@ -502,6 +505,8 @@ class MIoTLan:
|
||||
_lan_state_sub_map: dict[str, Callable[[bool], asyncio.Future]]
|
||||
_lan_ctrl_vote_map: dict[str, bool]
|
||||
|
||||
_profile_models: dict[str, dict]
|
||||
|
||||
_init_done: bool
|
||||
|
||||
def __init__(
|
||||
@ -597,6 +602,12 @@ class MIoTLan:
|
||||
if self._net_ifs.isdisjoint(self._available_net_ifs):
|
||||
_LOGGER.info('no valid net_ifs')
|
||||
return
|
||||
try:
|
||||
self._profile_models = load_yaml_file(
|
||||
yaml_file=gen_absolute_path(self.PROFILE_MODELS_FILE))
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.error('load profile models error, %s', err)
|
||||
self._profile_models = {}
|
||||
self._mev = MIoTEventLoop()
|
||||
self._queue = queue.Queue()
|
||||
self._cmd_event_fd = os.eventfd(0, os.O_NONBLOCK)
|
||||
@ -620,6 +631,7 @@ class MIoTLan:
|
||||
self.__lan_send_cmd(MIoTLanCmdType.DEINIT, None)
|
||||
self._thread.join()
|
||||
|
||||
self._profile_models = {}
|
||||
self._lan_devices = {}
|
||||
self._broadcast_socks = {}
|
||||
self._local_port = None
|
||||
@ -1032,6 +1044,19 @@ class MIoTLan:
|
||||
elif mips_cmd.type_ == MIoTLanCmdType.DEVICE_UPDATE:
|
||||
devices: dict[str, dict] = mips_cmd.data
|
||||
for did, info in devices.items():
|
||||
# did MUST be digit(UINT64)
|
||||
if not did.isdigit():
|
||||
_LOGGER.info('invalid did, %s', did)
|
||||
continue
|
||||
if (
|
||||
'model' not in info
|
||||
or info['model'] in self._profile_models):
|
||||
# Do not support the local control of
|
||||
# Profile device for the time being
|
||||
_LOGGER.info(
|
||||
'model not support local ctrl, %s, %s',
|
||||
did, info.get('model'))
|
||||
continue
|
||||
if did not in self._lan_devices:
|
||||
if 'token' not in info:
|
||||
_LOGGER.error(
|
||||
|
@ -129,7 +129,7 @@ class MIoTStorage:
|
||||
self, full_path: str, type_: type = bytes, with_hash_check: bool = True
|
||||
) -> Union[bytes, str, dict, list, None]:
|
||||
if not os.path.exists(full_path):
|
||||
_LOGGER.debug('load error, file not exists, %s', full_path)
|
||||
_LOGGER.debug('load error, file does not exist, %s', full_path)
|
||||
return None
|
||||
if not os.access(full_path, os.R_OK):
|
||||
_LOGGER.error('load error, file not readable, %s', full_path)
|
||||
@ -160,7 +160,7 @@ class MIoTStorage:
|
||||
if type_ in [dict, list]:
|
||||
return json.loads(data_bytes)
|
||||
_LOGGER.error(
|
||||
'load error, un-support data type, %s', type_.__name__)
|
||||
'load error, unsupported data type, %s', type_.__name__)
|
||||
return None
|
||||
except (OSError, TypeError) as e:
|
||||
_LOGGER.error('load error, %s, %s', e, traceback.format_exc())
|
||||
@ -219,8 +219,8 @@ class MIoTStorage:
|
||||
w_bytes = json.dumps(data).encode('utf-8')
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'save error, un-support data type, %s', type_.__name__)
|
||||
return None
|
||||
'save error, unsupported data type, %s', type_.__name__)
|
||||
return False
|
||||
with open(full_path, 'wb') as w_file:
|
||||
w_file.write(w_bytes)
|
||||
if with_hash:
|
||||
|
@ -3,14 +3,20 @@
|
||||
"urn:miot-spec-v2:property:air-cooler:000000EB": "open_close",
|
||||
"urn:miot-spec-v2:property:alarm:00000012": "open_close",
|
||||
"urn:miot-spec-v2:property:anion:00000025": "open_close",
|
||||
"urn:miot-spec-v2:property:anti-fake:00000130": "yes_no",
|
||||
"urn:miot-spec-v2:property:arrhythmia:000000B4": "yes_no",
|
||||
"urn:miot-spec-v2:property:auto-cleanup:00000124": "open_close",
|
||||
"urn:miot-spec-v2:property:auto-deodorization:00000125": "open_close",
|
||||
"urn:miot-spec-v2:property:auto-keep-warm:0000002B": "open_close",
|
||||
"urn:miot-spec-v2:property:automatic-feeding:000000F0": "open_close",
|
||||
"urn:miot-spec-v2:property:blow:000000CD": "open_close",
|
||||
"urn:miot-spec-v2:property:card-insertion-state:00000106": "yes_no",
|
||||
"urn:miot-spec-v2:property:contact-state:0000007C": "contact_state",
|
||||
"urn:miot-spec-v2:property:current-physical-control-lock:00000099": "open_close",
|
||||
"urn:miot-spec-v2:property:delay:0000014F": "yes_no",
|
||||
"urn:miot-spec-v2:property:deodorization:000000C6": "open_close",
|
||||
"urn:miot-spec-v2:property:dns-auto-mode:000000DC": "open_close",
|
||||
"urn:miot-spec-v2:property:current-physical-control-lock:00000099": "open_close",
|
||||
"urn:miot-spec-v2:property:driving-status:000000B9": "yes_no",
|
||||
"urn:miot-spec-v2:property:dryer:00000027": "open_close",
|
||||
"urn:miot-spec-v2:property:eco:00000024": "open_close",
|
||||
"urn:miot-spec-v2:property:glimmer-full-color:00000089": "open_close",
|
||||
@ -20,17 +26,25 @@
|
||||
"urn:miot-spec-v2:property:horizontal-swing:00000017": "open_close",
|
||||
"urn:miot-spec-v2:property:hot-water-recirculation:0000011C": "open_close",
|
||||
"urn:miot-spec-v2:property:image-distortion-correction:0000010F": "open_close",
|
||||
"urn:miot-spec-v2:property:mute:00000040": "open_close",
|
||||
"urn:miot-spec-v2:property:local-storage:0000011E": "yes_no",
|
||||
"urn:miot-spec-v2:property:motion-detection:00000056": "open_close",
|
||||
"urn:miot-spec-v2:property:motion-state:0000007D": "motion_state",
|
||||
"urn:miot-spec-v2:property:motion-tracking:0000008A": "open_close",
|
||||
"urn:miot-spec-v2:property:motor-reverse:00000072": "yes_no",
|
||||
"urn:miot-spec-v2:property:mute:00000040": "open_close",
|
||||
"urn:miot-spec-v2:property:off-delay:00000053": "open_close",
|
||||
"urn:miot-spec-v2:property:on:00000006": "open_close",
|
||||
"urn:miot-spec-v2:property:physical-controls-locked:0000001D": "open_close",
|
||||
"urn:miot-spec-v2:property:plasma:00000132": "yes_no",
|
||||
"urn:miot-spec-v2:property:preheat:00000103": "open_close",
|
||||
"urn:miot-spec-v2:property:seating-state:000000B8": "yes_no",
|
||||
"urn:miot-spec-v2:property:silent-execution:000000FB": "yes_no",
|
||||
"urn:miot-spec-v2:property:sleep-aid-mode:0000010B": "open_close",
|
||||
"urn:miot-spec-v2:property:sleep-mode:00000028": "open_close",
|
||||
"urn:miot-spec-v2:property:snore-state:0000012A": "yes_no",
|
||||
"urn:miot-spec-v2:property:soft-wind:000000CF": "open_close",
|
||||
"urn:miot-spec-v2:property:speed-control:000000E8": "open_close",
|
||||
"urn:miot-spec-v2:property:submersion-state:0000007E": "yes_no",
|
||||
"urn:miot-spec-v2:property:time-watermark:00000087": "open_close",
|
||||
"urn:miot-spec-v2:property:un-straight-blowing:00000100": "open_close",
|
||||
"urn:miot-spec-v2:property:uv:00000029": "open_close",
|
||||
@ -43,41 +57,19 @@
|
||||
"urn:miot-spec-v2:property:wdr-mode:00000088": "open_close",
|
||||
"urn:miot-spec-v2:property:wet:0000002A": "open_close",
|
||||
"urn:miot-spec-v2:property:wifi-band-combine:000000E0": "open_close",
|
||||
"urn:miot-spec-v2:property:anti-fake:00000130": "yes_no",
|
||||
"urn:miot-spec-v2:property:arrhythmia:000000B4": "yes_no",
|
||||
"urn:miot-spec-v2:property:card-insertion-state:00000106": "yes_no",
|
||||
"urn:miot-spec-v2:property:delay:0000014F": "yes_no",
|
||||
"urn:miot-spec-v2:property:driving-status:000000B9": "yes_no",
|
||||
"urn:miot-spec-v2:property:local-storage:0000011E": "yes_no",
|
||||
"urn:miot-spec-v2:property:motor-reverse:00000072": "yes_no",
|
||||
"urn:miot-spec-v2:property:plasma:00000132": "yes_no",
|
||||
"urn:miot-spec-v2:property:seating-state:000000B8": "yes_no",
|
||||
"urn:miot-spec-v2:property:silent-execution:000000FB": "yes_no",
|
||||
"urn:miot-spec-v2:property:snore-state:0000012A": "yes_no",
|
||||
"urn:miot-spec-v2:property:submersion-state:0000007E": "yes_no",
|
||||
"urn:miot-spec-v2:property:wifi-ssid-hidden:000000E3": "yes_no",
|
||||
"urn:miot-spec-v2:property:wind-reverse:00000117": "yes_no",
|
||||
"urn:miot-spec-v2:property:motion-state:0000007D": "motion_state",
|
||||
"urn:miot-spec-v2:property:contact-state:0000007C": "contact_state"
|
||||
"urn:miot-spec-v2:property:wind-reverse:00000117": "yes_no"
|
||||
},
|
||||
"translate": {
|
||||
"default": {
|
||||
"zh-Hans": {
|
||||
"true": "真",
|
||||
"false": "假"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"true": "真",
|
||||
"false": "假"
|
||||
"de": {
|
||||
"true": "Wahr",
|
||||
"false": "Falsch"
|
||||
},
|
||||
"en": {
|
||||
"true": "True",
|
||||
"false": "False"
|
||||
},
|
||||
"de": {
|
||||
"true": "Wahr",
|
||||
"false": "Falsch"
|
||||
},
|
||||
"es": {
|
||||
"true": "Verdadero",
|
||||
"false": "Falso"
|
||||
@ -86,32 +78,44 @@
|
||||
"true": "Vrai",
|
||||
"false": "Faux"
|
||||
},
|
||||
"ja": {
|
||||
"true": "真",
|
||||
"false": "偽"
|
||||
},
|
||||
"nl": {
|
||||
"true": "True",
|
||||
"false": "False"
|
||||
},
|
||||
"pt": {
|
||||
"true": "True",
|
||||
"false": "False"
|
||||
},
|
||||
"pt-BR": {
|
||||
"true": "True",
|
||||
"false": "False"
|
||||
},
|
||||
"ru": {
|
||||
"true": "Истина",
|
||||
"false": "Ложь"
|
||||
},
|
||||
"ja": {
|
||||
"zh-Hans": {
|
||||
"true": "真",
|
||||
"false": "偽"
|
||||
"false": "假"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"true": "真",
|
||||
"false": "假"
|
||||
}
|
||||
},
|
||||
"open_close": {
|
||||
"zh-Hans": {
|
||||
"true": "开启",
|
||||
"false": "关闭"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"true": "開啟",
|
||||
"false": "關閉"
|
||||
"de": {
|
||||
"true": "Öffnen",
|
||||
"false": "Schließen"
|
||||
},
|
||||
"en": {
|
||||
"true": "Open",
|
||||
"false": "Close"
|
||||
},
|
||||
"de": {
|
||||
"true": "Öffnen",
|
||||
"false": "Schließen"
|
||||
},
|
||||
"es": {
|
||||
"true": "Abierto",
|
||||
"false": "Cerrado"
|
||||
@ -120,32 +124,44 @@
|
||||
"true": "Ouvert",
|
||||
"false": "Fermer"
|
||||
},
|
||||
"ja": {
|
||||
"true": "開く",
|
||||
"false": "閉じる"
|
||||
},
|
||||
"nl": {
|
||||
"true": "Open",
|
||||
"false": "Dicht"
|
||||
},
|
||||
"pt": {
|
||||
"true": "Aberto",
|
||||
"false": "Fechado"
|
||||
},
|
||||
"pt-BR": {
|
||||
"true": "Aberto",
|
||||
"false": "Fechado"
|
||||
},
|
||||
"ru": {
|
||||
"true": "Открыть",
|
||||
"false": "Закрыть"
|
||||
},
|
||||
"ja": {
|
||||
"true": "開く",
|
||||
"false": "閉じる"
|
||||
"zh-Hans": {
|
||||
"true": "开启",
|
||||
"false": "关闭"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"true": "開啟",
|
||||
"false": "關閉"
|
||||
}
|
||||
},
|
||||
"yes_no": {
|
||||
"zh-Hans": {
|
||||
"true": "是",
|
||||
"false": "否"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"true": "是",
|
||||
"false": "否"
|
||||
"de": {
|
||||
"true": "Ja",
|
||||
"false": "Nein"
|
||||
},
|
||||
"en": {
|
||||
"true": "Yes",
|
||||
"false": "No"
|
||||
},
|
||||
"de": {
|
||||
"true": "Ja",
|
||||
"false": "Nein"
|
||||
},
|
||||
"es": {
|
||||
"true": "Sí",
|
||||
"false": "No"
|
||||
@ -154,32 +170,44 @@
|
||||
"true": "Oui",
|
||||
"false": "Non"
|
||||
},
|
||||
"ja": {
|
||||
"true": "はい",
|
||||
"false": "いいえ"
|
||||
},
|
||||
"nl": {
|
||||
"true": "Ja",
|
||||
"false": "Nee"
|
||||
},
|
||||
"pt": {
|
||||
"true": "Sim",
|
||||
"false": "Não"
|
||||
},
|
||||
"pt-BR": {
|
||||
"true": "Sim",
|
||||
"false": "Não"
|
||||
},
|
||||
"ru": {
|
||||
"true": "Да",
|
||||
"false": "Нет"
|
||||
},
|
||||
"ja": {
|
||||
"true": "はい",
|
||||
"false": "いいえ"
|
||||
"zh-Hans": {
|
||||
"true": "是",
|
||||
"false": "否"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"true": "是",
|
||||
"false": "否"
|
||||
}
|
||||
},
|
||||
"motion_state": {
|
||||
"zh-Hans": {
|
||||
"true": "有人",
|
||||
"false": "无人"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"true": "有人",
|
||||
"false": "無人"
|
||||
"de": {
|
||||
"true": "Bewegung erkannt",
|
||||
"false": "Keine Bewegung erkannt"
|
||||
},
|
||||
"en": {
|
||||
"true": "Motion Detected",
|
||||
"false": "No Motion Detected"
|
||||
},
|
||||
"de": {
|
||||
"true": "Bewegung erkannt",
|
||||
"false": "Keine Bewegung erkannt"
|
||||
},
|
||||
"es": {
|
||||
"true": "Movimiento detectado",
|
||||
"false": "No se detecta movimiento"
|
||||
@ -188,32 +216,44 @@
|
||||
"true": "Mouvement détecté",
|
||||
"false": "Aucun mouvement détecté"
|
||||
},
|
||||
"ja": {
|
||||
"true": "動きを検知",
|
||||
"false": "動きが検出されません"
|
||||
},
|
||||
"nl": {
|
||||
"true": "Contact",
|
||||
"false": "Geen contact"
|
||||
},
|
||||
"pt": {
|
||||
"true": "Contato",
|
||||
"false": "Sem contato"
|
||||
},
|
||||
"pt-BR": {
|
||||
"true": "Contato",
|
||||
"false": "Sem contato"
|
||||
},
|
||||
"ru": {
|
||||
"true": "Обнаружено движение",
|
||||
"false": "Движение не обнаружено"
|
||||
},
|
||||
"ja": {
|
||||
"true": "動きを検知",
|
||||
"false": "動きが検出されません"
|
||||
"zh-Hans": {
|
||||
"true": "有人",
|
||||
"false": "无人"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"true": "有人",
|
||||
"false": "無人"
|
||||
}
|
||||
},
|
||||
"contact_state": {
|
||||
"zh-Hans": {
|
||||
"true": "接触",
|
||||
"false": "分离"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"true": "接觸",
|
||||
"false": "分離"
|
||||
"de": {
|
||||
"true": "Kontakt",
|
||||
"false": "Kein Kontakt"
|
||||
},
|
||||
"en": {
|
||||
"true": "Contact",
|
||||
"false": "No Contact"
|
||||
},
|
||||
"de": {
|
||||
"true": "Kontakt",
|
||||
"false": "Kein Kontakt"
|
||||
},
|
||||
"es": {
|
||||
"true": "Contacto",
|
||||
"false": "Sin contacto"
|
||||
@ -222,13 +262,33 @@
|
||||
"true": "Contact",
|
||||
"false": "Pas de contact"
|
||||
},
|
||||
"ja": {
|
||||
"true": "接触",
|
||||
"false": "非接触"
|
||||
},
|
||||
"nl": {
|
||||
"true": "Contact",
|
||||
"false": "Geen contact"
|
||||
},
|
||||
"pt": {
|
||||
"true": "Contato",
|
||||
"false": "Sem contato"
|
||||
},
|
||||
"pt-BR": {
|
||||
"true": "Contato",
|
||||
"false": "Sem contato"
|
||||
},
|
||||
"ru": {
|
||||
"true": "Контакт",
|
||||
"false": "Нет контакта"
|
||||
},
|
||||
"ja": {
|
||||
"zh-Hans": {
|
||||
"true": "接触",
|
||||
"false": "非接触"
|
||||
"false": "分离"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"true": "接觸",
|
||||
"false": "分離"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,27 @@
|
||||
{
|
||||
"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",
|
||||
@ -66,50 +88,6 @@
|
||||
"service:004:event:001": "Événement virtuel survenu",
|
||||
"service:004:property:001": "Nom de l'événement"
|
||||
},
|
||||
"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": "Название события"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"ja": {
|
||||
"service:001": "デバイス情報",
|
||||
"service:001:property:003": "デバイスID",
|
||||
@ -132,6 +110,28 @@
|
||||
"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",
|
||||
|
@ -1,26 +1,22 @@
|
||||
{
|
||||
"urn:miot-spec-v2:device:health-pot:0000A051:chunmi-a1": {
|
||||
"urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4": {
|
||||
"properties": [
|
||||
"9.*",
|
||||
"13.*",
|
||||
"15.*"
|
||||
],
|
||||
"services": [
|
||||
"5"
|
||||
"10"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01": {
|
||||
"properties": [
|
||||
"5.1"
|
||||
],
|
||||
"services": [
|
||||
"4",
|
||||
"7",
|
||||
"8"
|
||||
],
|
||||
"properties": [
|
||||
"5.1"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:light:0000A001:philips-strip3": {
|
||||
"services": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"properties": [
|
||||
"2.2"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": {
|
||||
@ -28,10 +24,18 @@
|
||||
"2.1"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1": {
|
||||
"urn:miot-spec-v2:device:health-pot:0000A051:chunmi-a1": {
|
||||
"services": [
|
||||
"5"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:light:0000A001:philips-strip3": {
|
||||
"properties": [
|
||||
"2.2"
|
||||
],
|
||||
"services": [
|
||||
"1",
|
||||
"5"
|
||||
"3"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:light:0000A001:yeelink-color2": {
|
||||
@ -50,14 +54,10 @@
|
||||
"3"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4": {
|
||||
"urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1": {
|
||||
"services": [
|
||||
"10"
|
||||
],
|
||||
"properties": [
|
||||
"9.*",
|
||||
"13.*",
|
||||
"15.*"
|
||||
"1",
|
||||
"5"
|
||||
]
|
||||
}
|
||||
}
|
@ -119,7 +119,7 @@ class Notify(MIoTActionEntity, NotifyEntity):
|
||||
in_value: list[dict] = []
|
||||
for index, prop in enumerate(self.spec.in_):
|
||||
if type(in_list[index]).__name__ != prop.format_:
|
||||
logging.error(
|
||||
_LOGGER.error(
|
||||
'action exec failed, %s(%s), invalid params item, '
|
||||
'which item(%s) in the list must be %s, %s',
|
||||
self.name, self.entity_id, prop.description_trans,
|
||||
|
@ -45,9 +45,7 @@
|
||||
"get_cert_error": "ゲートウェイ証明書を取得できませんでした。",
|
||||
"no_family_selected": "家庭が選択されていません。",
|
||||
"no_devices": "選択された家庭にデバイスがありません。デバイスがある家庭を選択して続行してください。",
|
||||
"no_central_device": "【中央ゲートウェイモード】Home Assistant が存在する LAN 内に使用可能な Xiaomi 中央ゲートウェイがある必要があります。選択された家庭がこの要件を満たしているかどうかを確認してください。",
|
||||
"update_config_error": "設定情報の更新に失敗しました。",
|
||||
"not_confirm": "変更項目が確認されていません。確認を選択してから送信してください。"
|
||||
"no_central_device": "【中央ゲートウェイモード】Home Assistant が存在する LAN 内に使用可能な Xiaomi 中央ゲートウェイがある必要があります。選択された家庭がこの要件を満たしているかどうかを確認してください。"
|
||||
},
|
||||
"abort": {
|
||||
"network_connect_error": "設定に失敗しました。ネットワーク接続に異常があります。デバイスのネットワーク設定を確認してください。",
|
||||
|
144
custom_components/xiaomi_home/translations/nl.json
Normal file
144
custom_components/xiaomi_home/translations/nl.json
Normal file
@ -0,0 +1,144 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Xiaomi Home Integratie",
|
||||
"step": {
|
||||
"eula": {
|
||||
"title": "Risiconotitie",
|
||||
"description": "1. Uw Xiaomi-gebruikersinformatie en apparaatinformatie worden opgeslagen in het Home Assistant-systeem. **Xiaomi kan de beveiliging van het opslagmechanisme van Home Assistant niet garanderen**. U bent verantwoordelijk voor het voorkomen dat uw informatie wordt gestolen.\r\n2. Deze integratie wordt onderhouden door de open-sourcegemeenschap. Er kunnen stabiliteitsproblemen of andere problemen optreden. Bij problemen of fouten met deze integratie, **moet u hulp zoeken bij de open-sourcegemeenschap in plaats van contact op te nemen met de Xiaomi klantenservice**.\r\n3. U heeft enige technische vaardigheden nodig om uw lokale werkomgeving te onderhouden. De integratie is niet gebruiksvriendelijk voor beginners.\r\n4. Lees het README-bestand voordat u begint.\n\n5. Om een stabiel gebruik van de integratie te waarborgen en misbruik van de interface te voorkomen, **mag deze integratie alleen worden gebruikt in Home Assistant. Voor details, zie de LICENSE**.",
|
||||
"data": {
|
||||
"eula": "Ik ben me bewust van de bovenstaande risico's en bereid om vrijwillig alle risico's die gepaard gaan met het gebruik van de integratie te aanvaarden."
|
||||
}
|
||||
},
|
||||
"auth_config": {
|
||||
"title": "Basisconfiguratie",
|
||||
"description": "### Inlogregio\r\nSelecteer de regio van uw Xiaomi-account. U kunt deze vinden in de Xiaomi Home APP > Profiel (onderin het menu) > Extra instellingen > Over Xiaomi Home.\r\n### Taal\r\nKies de taal voor de apparaats- en entiteitsnamen. Sommige zinnen zonder vertaling worden in het Engels weergegeven.\r\n### OAuth2 Omleidings-URL\r\nHet OAuth2 authenticatie omleidingsadres is **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant moet zich in hetzelfde lokale netwerk bevinden als de huidige werkterminal (bijv. de persoonlijke computer) en de werkterminal moet toegang hebben tot de startpagina van Home Assistant via dit adres. Anders kan de inlogauthenticatie mislukken.\r\n### Opmerking\r\n- Voor gebruikers met honderden of meer Mi Home-apparaten kan het aanvankelijke toevoegen van de integratie enige tijd duren. Wees geduldig.\r\n- Als Home Assistant draait in een Docker-omgeving, zorg er dan voor dat de Docker-netwerkmodus is ingesteld op host, anders werkt de lokale controlefunctionaliteit mogelijk niet correct.\r\n- De lokale controlefunctionaliteit van de integratie heeft enkele afhankelijkheden. Lees het README zorgvuldig.",
|
||||
"data": {
|
||||
"cloud_server": "Inlogregio",
|
||||
"integration_language": "Taal",
|
||||
"oauth_redirect_url": "OAuth2 Omleidings-URL"
|
||||
}
|
||||
},
|
||||
"oauth_error": {
|
||||
"title": "Inlogfout",
|
||||
"description": "Klik OP VOLGENDE om het opnieuw te proberen."
|
||||
},
|
||||
"devices_filter": {
|
||||
"title": "Selecteer Huis en Apparaten",
|
||||
"description": "## Gebruiksinstructies\r\n### Controlemodus\r\n- Auto: Wanneer er een beschikbare Xiaomi centrale hubgateway in het lokale netwerk is, geeft Home Assistant de voorkeur aan het verzenden van apparaatbedieningscommando's via de centrale hubgateway om lokale controle te bereiken. Als er geen centrale hubgateway in het lokale netwerk is, zal het proberen bedieningscommando's te verzenden via de Xiaomi LAN-controlefunctie. Alleen wanneer de bovenstaande lokale controlevoorwaarden niet zijn vervuld, worden de apparaatbedieningscommando's via de cloud verzonden.\r\n- Cloud: Alle bedieningscommando's worden via de cloud verzonden.\r\n### Apparaten importeren vanuit huis\r\nDe integratie voegt apparaten toe van de geselecteerde huizen.\n### Ruimtenaamsynchronisatiemodus\nBij het importeren van apparaten vanuit de Xiaomi Home APP naar Home Assistant is de naamgevingsconventie van het gebied waarin het apparaat wordt toegevoegd als volgt. Opmerking: het synchronisatieproces van het apparaat verandert de huis- of ruimte-instellingen in de Xiaomi Home APP niet.\r\n- Niet synchroniseren: Het apparaat wordt aan geen enkel gebied toegevoegd.\r\n- Andere opties: Het apparaat wordt toegevoegd aan een gebied dat is genoemd naar de huis- en/of ruimtenamen die al bestaan in de Xiaomi Home APP.\r\n### Debugmodus voor actie\r\nVoor de actie gedefinieerd in MIoT-Spec-V2 van het apparaat, wordt er een Tekstentiteit samen met een Notificatie-entiteit aangemaakt, waarin u bedieningscommando's naar het apparaat kunt sturen voor debugging.\r\n### Verberg niet-standaard gemaakte entiteiten\r\nVerberg de entiteiten die zijn gegenereerd vanuit niet-standaard MIoT-Spec-V2-instanties, waarvan de namen beginnen met \"*\".\r\n\r\n \r\n### Hallo {nick_name}, selecteer alstublieft de integratie controlemethodiek en het huis waar het apparaat dat u wilt importeren zich bevindt.",
|
||||
"data": {
|
||||
"ctrl_mode": "Controlemodus",
|
||||
"home_infos": "Importeer apparaten uit huis",
|
||||
"area_name_rule": "Ruimtenaamsynchronisatiemodus",
|
||||
"action_debug": "Debugmodus voor actie",
|
||||
"hide_non_standard_entities": "Verberg niet-standaard gemaakte entiteiten"
|
||||
}
|
||||
}
|
||||
},
|
||||
"progress": {
|
||||
"oauth": "### {link_left}Klik hier om in te loggen{link_right}\r\n(U wordt automatisch doorgestuurd naar de volgende pagina na succesvolle inlog)"
|
||||
},
|
||||
"error": {
|
||||
"eula_not_agree": "Lees de risiconotitie.",
|
||||
"get_token_error": "Mislukt bij het ophalen van inlogautorisatie-informatie (OAuth-token).",
|
||||
"get_homeinfo_error": "Mislukt bij het ophalen van huisinformatie.",
|
||||
"mdns_discovery_error": "Lokaal apparaatsontdekkingsservice-exceptie.",
|
||||
"get_cert_error": "Mislukt bij het ophalen van het certificaat van de centrale hubgateway.",
|
||||
"no_family_selected": "Geen huis geselecteerd.",
|
||||
"no_devices": "Het geselecteerde huis heeft geen apparaten. Kies a.u.b. een huis met apparaten en ga verder.",
|
||||
"no_central_device": "[Centrale Hub Gateway Modus] vereist een beschikbare Xiaomi centrale hubgateway in het lokale netwerk waar Home Assistant zich bevindt. Controleer of het geselecteerde huis aan deze vereiste voldoet."
|
||||
},
|
||||
"abort": {
|
||||
"network_connect_error": "Configuratie mislukt. De netwerkverbinding is abnormaal. Controleer de netwerkinstellingen van de apparatuur.",
|
||||
"already_configured": "Configuratie voor deze gebruiker is al voltooid. Ga naar de integratiepagina en klik op de CONFIGUREER-knop om wijzigingen aan te brengen.",
|
||||
"invalid_auth_info": "Authenticatie-informatie is verlopen. Ga naar de integratiepagina en klik op de CONFIGUREER-knop om opnieuw te authentiseren.",
|
||||
"config_flow_error": "Integratie configuratiefout: {error}."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"auth_config": {
|
||||
"title": "Authenticatieconfiguratie",
|
||||
"description": "Lokale authenticatie-informatie is verlopen. Begin alstublieft het authenticatieproces opnieuw.\r\n### Huidige inlogregio: {cloud_server}\r\n### OAuth2 Omleidings-URL\r\nHet OAuth2 authenticatie omleidingsadres is **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant moet zich in hetzelfde lokale netwerk bevinden als de huidige werkterminal (bijv. de persoonlijke computer) en de werkterminal moet toegang hebben tot de startpagina van Home Assistant via dit adres. Anders kan de inlogauthenticatie mislukken.",
|
||||
"data": {
|
||||
"oauth_redirect_url": "OAuth2 Omleidings-URL"
|
||||
}
|
||||
},
|
||||
"oauth_error": {
|
||||
"title": "Er is een fout opgetreden tijdens het inloggen.",
|
||||
"description": "Klik OP VOLGENDE om opnieuw te proberen."
|
||||
},
|
||||
"config_options": {
|
||||
"title": "Configuratie-opties",
|
||||
"description": "### Hallo, {nick_name}\r\n\r\nXiaomi ID: {uid}\r\nHuidige inlogregio: {cloud_server}\r\n\r\nKies de opties die u wilt configureren en klik vervolgens op VOLGENDE.",
|
||||
"data": {
|
||||
"integration_language": "Integratietaal",
|
||||
"update_user_info": "Werk gebruikersinformatie bij",
|
||||
"update_devices": "Werk apparatenlijst bij",
|
||||
"action_debug": "Debugmodus voor actie",
|
||||
"hide_non_standard_entities": "Verberg niet-standaard gemaakte entiteiten",
|
||||
"update_trans_rules": "Werk entiteitsconversieregels bij",
|
||||
"update_lan_ctrl_config": "Werk LAN controleconfiguratie bij"
|
||||
}
|
||||
},
|
||||
"update_user_info": {
|
||||
"title": "Bijwerken van gebruikersnickname",
|
||||
"description": "Hallo {nick_name}, u kunt uw aangepaste bijnaam hieronder wijzigen.",
|
||||
"data": {
|
||||
"nick_name": "Bijnaam"
|
||||
}
|
||||
},
|
||||
"devices_filter": {
|
||||
"title": "Huis en Apparaten opnieuw selecteren",
|
||||
"description": "## Gebruiksinstructies\r\n### Controlemodus\r\n- Auto: Wanneer er een beschikbare Xiaomi centrale hubgateway in het lokale netwerk is, geeft Home Assistant de voorkeur aan het verzenden van apparaatbedieningscommando's via de centrale hubgateway om lokale controle te bereiken. Als er geen centrale hubgateway in het lokale netwerk is, zal het proberen bedieningscommando's te verzenden via de Xiaomi LAN-controlefunctie. Alleen wanneer de bovenstaande lokale controlevoorwaarden niet zijn vervuld, worden de apparaatbedieningscommando's via de cloud verzonden.\r\n- Cloud: Alle bedieningscommando's worden via de cloud verzonden.\r\n### Apparaten importeren vanuit huis\r\nDe integratie voegt apparaten toe van de geselecteerde huizen.\r\n \r\n### Hallo {nick_name}, selecteer alstublieft de integratie controlemethodiek en het huis waar het apparaat dat u wilt importeren zich bevindt.",
|
||||
"data": {
|
||||
"ctrl_mode": "Controlemodus",
|
||||
"home_infos": "Importeer apparaten uit huis"
|
||||
}
|
||||
},
|
||||
"update_trans_rules": {
|
||||
"title": "Bijwerken van entiteiten transformateregels",
|
||||
"description": "## Gebruiksinstructies\r\n- Werk de entiteitsinformatie van apparaten in de huidige integratie-instantie bij, inclusief MIoT-Spec-V2 meertalige configuratie, booleanvertaling en modelfiltering.\r\n- **Waarschuwing**: Dit is een globale configuratie en zal de lokale cache bijwerken. Dit zal alle integratie-instanties beïnvloeden.\r\n- Deze handeling duurt enige tijd, wees geduldig. Vink \"Bevestig bijwerken\" aan en klik op \"Volgende\" om **{urn_count}** regels bij te werken, anders overslaan.",
|
||||
"data": {
|
||||
"confirm": "Bevestig de update"
|
||||
}
|
||||
},
|
||||
"update_lan_ctrl_config": {
|
||||
"title": "Update LAN controleconfiguratie",
|
||||
"description": "## Gebruiksinstructies\r\nWerk de configuraties voor de Xiaomi LAN controlefunctie bij. Wanneer de cloud en de centrale hubgateway de apparaten niet kunnen bedienen, zal de integratie proberen de apparaten via het LAN te bedienen. Als er geen netwerkkaart is geselecteerd, zal de LAN controlefunctie niet werken.\r\n- Alleen MIoT-Spec-V2 compatibele IP-apparaten in het LAN worden ondersteund. Sommige apparaten die vóór 2020 zijn geproduceerd, ondersteunen mogelijk geen LAN controle of LAN abonnement.\r\n- Selecteer de netwerkkaart(en) op hetzelfde netwerk als de te bedienen apparaten. Meerdere netwerkkaarten kunnen worden geselecteerd. Als Home Assistant vanwege de meervoudige selectie van de netwerkkaarten twee of meer verbindingen heeft met het lokale netwerk, wordt aanbevolen om de verbinding met de beste netwerkverbinding te selecteren, anders kan dit een negatief effect hebben op de apparaten.\r\n- Als er terminalapparaten (Xiaomi-luidsprekers met scherm, mobiele telefoons, enz.) in het LAN zijn die lokale controle ondersteunen, kan het inschakelen van LAN-abonnement leiden tot lokale automatisering- en apparaatanomalieën.\r\n- **Waarschuwing**: Dit is een globale configuratie. Het zal alle integratie-instanties beïnvloeden. Gebruik het met voorzichtigheid.\r\n{notice_net_dup}",
|
||||
"data": {
|
||||
"net_interfaces": "Selecteer alstublieft de te gebruiken netwerkkaart",
|
||||
"enable_subscribe": "Zet LAN-abonnement aan"
|
||||
}
|
||||
},
|
||||
"config_confirm": {
|
||||
"title": "Bevestig Configuratie",
|
||||
"description": "Hallo **{nick_name}**, bevestig alstublieft de nieuwste configuratie-informatie en klik vervolgens op INDENKEN.\r\nDe integratie zal opnieuw laden met de bijgewerkte configuratie.\r\n\r\nIntegratietaal: \t{lang_new}\r\nBijnaam: \t{nick_name_new}\r\nDebugmodus voor actie: \t{action_debug}\r\nVerberg niet-standaard gemaakte entiteiten: \t{hide_non_standard_entities}\r\nWijzigingen in apparaten: \tVoeg **{devices_add}** apparaten toe, Verwijder **{devices_remove}** apparaten\r\nWijzigingen in transformateregels: \tEr zijn in totaal **{trans_rules_count}** regels, en **{trans_rules_count_success}** regels zijn bijgewerkt",
|
||||
"data": {
|
||||
"confirm": "Bevestig de wijziging"
|
||||
}
|
||||
}
|
||||
},
|
||||
"progress": {
|
||||
"oauth": "### {link_left}Klik hier om opnieuw in te loggen{link_right}"
|
||||
},
|
||||
"error": {
|
||||
"not_auth": "Niet geauthenticeerd. Klik op de authenticatielink om de gebruikersidentiteit te verifiëren.",
|
||||
"get_token_error": "Mislukt bij het ophalen van inlogautorisatie-informatie (OAuth-token).",
|
||||
"get_homeinfo_error": "Mislukt bij het ophalen van huisinformatie.",
|
||||
"get_cert_error": "Mislukt bij het ophalen van het certificaat van de centrale hubgateway.",
|
||||
"no_devices": "Het geselecteerde huis heeft geen apparaten. Kies a.u.b. een huis met apparaten en ga verder.",
|
||||
"no_family_selected": "Geen huis geselecteerd.",
|
||||
"no_central_device": "[Centrale Hub Gateway Modus] vereist een beschikbare Xiaomi centrale hubgateway in het lokale netwerk waar Home Assistant zich bevindt. Controleer of het geselecteerde huis aan deze vereiste voldoet.",
|
||||
"mdns_discovery_error": "Lokaal apparaatsontdekkingsservice-exceptie.",
|
||||
"update_config_error": "Mislukt bij het bijwerken van configuratie-informatie.",
|
||||
"not_confirm": "Wijzigingen zijn niet bevestigd. Bevestig de wijziging voordat u deze indient."
|
||||
},
|
||||
"abort": {
|
||||
"network_connect_error": "Configuratie mislukt. De netwerkverbinding is abnormaal. Controleer de netwerkinstellingen van de apparatuur.",
|
||||
"options_flow_error": "Integratie herconfiguratiefout: {error}",
|
||||
"re_add": "Voeg de integratie opnieuw toe. Foutmelding: {error}",
|
||||
"storage_error": "Integratie opslagmodule-exceptie. Probeer het opnieuw of voeg de integratie opnieuw toe: {error}",
|
||||
"inconsistent_account": "Accountinformatie is inconsistent. Log in met het juiste account."
|
||||
}
|
||||
}
|
||||
}
|
@ -45,13 +45,13 @@
|
||||
"get_cert_error": "Не удалось получить сертификат центрального шлюза.",
|
||||
"no_family_selected": "Не выбрана домашняя сеть.",
|
||||
"no_devices": "В выбранной домашней сети нет устройств. Пожалуйста, выберите домашнюю сеть с устройствами и продолжайте.",
|
||||
"no_central_device": "Для режима центрального шлюза Xiaomi необходимо наличие доступного центрального шлюза Xiaomi в локальной сети Home Assistant. Проверьте, соответствует ли выбранная домашняя сеть этому требованию.",
|
||||
"abort": {
|
||||
"network_connect_error": "Ошибка настройки. Сетевое подключение недоступно. Проверьте настройки сети устройства.",
|
||||
"already_configured": "Этот пользователь уже настроен. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы изменить настройки.",
|
||||
"invalid_auth_info": "Информация об авторизации истекла. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы переавторизоваться.",
|
||||
"config_flow_error": "Ошибка настройки интеграции: {error}"
|
||||
}
|
||||
"no_central_device": "Для режима центрального шлюза Xiaomi необходимо наличие доступного центрального шлюза Xiaomi в локальной сети Home Assistant. Проверьте, соответствует ли выбранная домашняя сеть этому требованию."
|
||||
},
|
||||
"abort": {
|
||||
"network_connect_error": "Ошибка настройки. Сетевое подключение недоступно. Проверьте настройки сети устройства.",
|
||||
"already_configured": "Этот пользователь уже настроен. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы изменить настройки.",
|
||||
"invalid_auth_info": "Информация об авторизации истекла. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы переавторизоваться.",
|
||||
"config_flow_error": "Ошибка настройки интеграции: {error}"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 贡献指南
|
||||
|
||||
[English](./CONTRIBUTING.md) | [简体中文](./CONTRIBUTING_zh.md)
|
||||
[English](../CONTRIBUTING.md) | [简体中文](./CONTRIBUTING_zh.md)
|
||||
|
||||
感谢您考虑为我们的项目做出贡献!您的努力将使我们的项目变得更好。
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
> Home Assistant 版本要求:
|
||||
>
|
||||
> - Core $\geq$ 2024.11.0
|
||||
> - Core $\geq$ 2024.4.4
|
||||
> - Operating System $\geq$ 13.0
|
||||
|
||||
### 方法 1:使用 git clone 命令从 GitHub 下载
|
||||
@ -378,8 +378,8 @@ siid、piid、eiid、aiid、value 均为十进制三位整数。
|
||||
## 文档
|
||||
|
||||
- [许可证](../LICENSE.md)
|
||||
- 贡献指南: [English](./CONTRIBUTING.md) | [简体中文](./CONTRIBUTING_zh.md)
|
||||
- [更新日志](./CHANGELOG.md)
|
||||
- 贡献指南: [English](../CONTRIBUTING.md) | [简体中文](./CONTRIBUTING_zh.md)
|
||||
- [更新日志](../CHANGELOG.md)
|
||||
- 开发文档: https://developers.home-assistant.io/docs/creating_component_index
|
||||
|
||||
## 目录结构
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Xiaomi Home",
|
||||
"homeassistant": "2024.11.0",
|
||||
"homeassistant": "2024.4.4",
|
||||
"hacs": "1.34.0"
|
||||
}
|
||||
|
17
install.sh
17
install.sh
@ -14,14 +14,21 @@ if [ ! -d "$config_path" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove the old version.
|
||||
rm -rf "$config_path/custom_components/xiaomi_home"
|
||||
# Get the script path.
|
||||
script_path=$(dirname "$0")
|
||||
# Change to the script path.
|
||||
cd "$script_path"
|
||||
|
||||
# Set source and target
|
||||
component_name=xiaomi_home
|
||||
source_path="$script_path/custom_components/$component_name"
|
||||
target_root="$config_path/custom_components"
|
||||
target_path="$target_root/$component_name"
|
||||
|
||||
# Remove the old version.
|
||||
rm -rf "$target_path"
|
||||
|
||||
# Copy the new version.
|
||||
cp -r custom_components/xiaomi_home/ "$config_path/custom_components/"
|
||||
mkdir -p "$target_root"
|
||||
cp -r "$source_path" "$target_path"
|
||||
|
||||
# Done.
|
||||
echo "Xiaomi Home installation is completed. Please restart Home Assistant."
|
||||
|
@ -4,8 +4,22 @@ import json
|
||||
from os import listdir, path
|
||||
from typing import Optional
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
SOURCE_DIR: str = path.dirname(path.abspath(__file__))
|
||||
ROOT_PATH: str = path.dirname(path.abspath(__file__))
|
||||
TRANS_RELATIVE_PATH: str = path.join(
|
||||
ROOT_PATH, '../custom_components/xiaomi_home/translations')
|
||||
MIOT_I18N_RELATIVE_PATH: str = path.join(
|
||||
ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n')
|
||||
SPEC_BOOL_TRANS_FILE = path.join(
|
||||
ROOT_PATH,
|
||||
'../custom_components/xiaomi_home/miot/specs/bool_trans.json')
|
||||
SPEC_MULTI_LANG_FILE = path.join(
|
||||
ROOT_PATH,
|
||||
'../custom_components/xiaomi_home/miot/specs/multi_lang.json')
|
||||
SPEC_FILTER_FILE = path.join(
|
||||
ROOT_PATH,
|
||||
'../custom_components/xiaomi_home/miot/specs/spec_filter.json')
|
||||
|
||||
|
||||
def load_json_file(file_path: str) -> Optional[dict]:
|
||||
@ -20,6 +34,23 @@ def load_json_file(file_path: str) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
def save_json_file(file_path: str, data: dict) -> None:
|
||||
with open(file_path, 'w', encoding='utf-8') as file:
|
||||
json.dump(data, file, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
def load_yaml_file(file_path: str) -> Optional[dict]:
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
return yaml.safe_load(file)
|
||||
except FileNotFoundError:
|
||||
print(file_path, 'is not found.')
|
||||
return None
|
||||
except yaml.YAMLError:
|
||||
print(file_path, 'is not a valid YAML file.')
|
||||
return None
|
||||
|
||||
|
||||
def dict_str_str(d: dict) -> bool:
|
||||
"""restricted format: dict[str, str]"""
|
||||
if not isinstance(d, dict):
|
||||
@ -83,45 +114,180 @@ def bool_trans(d: dict) -> bool:
|
||||
return False
|
||||
if not nested_3_dict_str_str(d['translate']):
|
||||
return False
|
||||
default_trans: dict = d['translate'].pop('default')
|
||||
if not default_trans:
|
||||
print('default trans is empty')
|
||||
return False
|
||||
default_keys: set[str] = set(default_trans.keys())
|
||||
for key, trans in d['translate'].items():
|
||||
trans_keys: set[str] = set(trans.keys())
|
||||
if set(trans.keys()) != default_keys:
|
||||
print('bool trans inconsistent', key, default_keys, trans_keys)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def compare_dict_structure(dict1: dict, dict2: dict) -> bool:
|
||||
if not isinstance(dict1, dict) or not isinstance(dict2, dict):
|
||||
print('invalid type')
|
||||
return False
|
||||
if dict1.keys() != dict2.keys():
|
||||
print('inconsistent key values, ', dict1.keys(), dict2.keys())
|
||||
return False
|
||||
for key in dict1:
|
||||
if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
|
||||
if not compare_dict_structure(dict1[key], dict2[key]):
|
||||
print('inconsistent key values, dict, ', key)
|
||||
return False
|
||||
elif isinstance(dict1[key], list) and isinstance(dict2[key], list):
|
||||
if not all(
|
||||
isinstance(i, type(j))
|
||||
for i, j in zip(dict1[key], dict2[key])):
|
||||
print('inconsistent key values, list, ', key)
|
||||
return False
|
||||
elif not isinstance(dict1[key], type(dict2[key])):
|
||||
print('inconsistent key values, type, ', key)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def sort_bool_trans(file_path: str):
|
||||
trans_data: dict = load_json_file(file_path=file_path)
|
||||
trans_data['data'] = dict(sorted(trans_data['data'].items()))
|
||||
for key, trans in trans_data['translate'].items():
|
||||
trans_data['translate'][key] = dict(sorted(trans.items()))
|
||||
return trans_data
|
||||
|
||||
|
||||
def sort_multi_lang(file_path: str):
|
||||
multi_lang: dict = load_json_file(file_path=file_path)
|
||||
multi_lang = dict(sorted(multi_lang.items()))
|
||||
for urn, trans in multi_lang.items():
|
||||
multi_lang[urn] = dict(sorted(trans.items()))
|
||||
for lang, spec in multi_lang[urn].items():
|
||||
multi_lang[urn][lang] = dict(sorted(spec.items()))
|
||||
return multi_lang
|
||||
|
||||
|
||||
def sort_spec_filter(file_path: str):
|
||||
filter_data: dict = load_json_file(file_path=file_path)
|
||||
filter_data = dict(sorted(filter_data.items()))
|
||||
for urn, spec in filter_data.items():
|
||||
filter_data[urn] = dict(sorted(spec.items()))
|
||||
return filter_data
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_bool_trans():
|
||||
data: dict = load_json_file(
|
||||
path.join(
|
||||
SOURCE_DIR,
|
||||
'../custom_components/xiaomi_home/miot/specs/bool_trans.json'))
|
||||
assert data
|
||||
assert bool_trans(data)
|
||||
data: dict = load_json_file(SPEC_BOOL_TRANS_FILE)
|
||||
assert data, f'load {SPEC_BOOL_TRANS_FILE} failed'
|
||||
assert bool_trans(data), f'{SPEC_BOOL_TRANS_FILE} format error'
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_spec_filter():
|
||||
data: dict = load_json_file(
|
||||
path.join(
|
||||
SOURCE_DIR,
|
||||
'../custom_components/xiaomi_home/miot/specs/spec_filter.json'))
|
||||
assert data
|
||||
assert spec_filter(data)
|
||||
data: dict = load_json_file(SPEC_FILTER_FILE)
|
||||
assert data, f'load {SPEC_FILTER_FILE} failed'
|
||||
assert spec_filter(data), f'{SPEC_FILTER_FILE} format error'
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_multi_lang():
|
||||
data: dict = load_json_file(
|
||||
path.join(
|
||||
SOURCE_DIR,
|
||||
'../custom_components/xiaomi_home/miot/specs/multi_lang.json'))
|
||||
assert data
|
||||
assert nested_3_dict_str_str(data)
|
||||
data: dict = load_json_file(SPEC_MULTI_LANG_FILE)
|
||||
assert data, f'load {SPEC_MULTI_LANG_FILE} failed'
|
||||
assert nested_3_dict_str_str(data), f'{SPEC_MULTI_LANG_FILE} format error'
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_miot_i18n():
|
||||
i18n_path: str = path.join(
|
||||
SOURCE_DIR, '../custom_components/xiaomi_home/miot/i18n')
|
||||
for file_name in listdir(i18n_path):
|
||||
file_path: str = path.join(i18n_path, file_name)
|
||||
for file_name in listdir(MIOT_I18N_RELATIVE_PATH):
|
||||
file_path: str = path.join(MIOT_I18N_RELATIVE_PATH, file_name)
|
||||
data: dict = load_json_file(file_path)
|
||||
assert data
|
||||
assert nested_3_dict_str_str(data)
|
||||
assert data, f'load {file_path} failed'
|
||||
assert nested_3_dict_str_str(data), f'{file_path} format error'
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_translations():
|
||||
for file_name in listdir(TRANS_RELATIVE_PATH):
|
||||
file_path: str = path.join(TRANS_RELATIVE_PATH, file_name)
|
||||
data: dict = load_json_file(file_path)
|
||||
assert data, f'load {file_path} failed'
|
||||
assert dict_str_dict(data), f'{file_path} format error'
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_miot_lang_integrity():
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from miot.const import INTEGRATION_LANGUAGES
|
||||
integration_lang_list: list[str] = [
|
||||
f'{key}.json' for key in list(INTEGRATION_LANGUAGES.keys())]
|
||||
translations_names: set[str] = set(listdir(TRANS_RELATIVE_PATH))
|
||||
assert len(translations_names) == len(integration_lang_list)
|
||||
assert translations_names == set(integration_lang_list)
|
||||
i18n_names: set[str] = set(listdir(MIOT_I18N_RELATIVE_PATH))
|
||||
assert len(i18n_names) == len(translations_names)
|
||||
assert i18n_names == translations_names
|
||||
bool_trans_data: set[str] = load_json_file(SPEC_BOOL_TRANS_FILE)
|
||||
bool_trans_names: set[str] = set(
|
||||
bool_trans_data['translate']['default'].keys())
|
||||
assert len(bool_trans_names) == len(translations_names)
|
||||
# Check translation files structure
|
||||
default_dict: dict = load_json_file(
|
||||
path.join(TRANS_RELATIVE_PATH, integration_lang_list[0]))
|
||||
for name in list(integration_lang_list)[1:]:
|
||||
compare_dict: dict = load_json_file(
|
||||
path.join(TRANS_RELATIVE_PATH, name))
|
||||
if not compare_dict_structure(default_dict, compare_dict):
|
||||
print('compare_dict_structure failed /translations, ', name)
|
||||
assert False
|
||||
# Check i18n files structure
|
||||
default_dict = load_json_file(
|
||||
path.join(MIOT_I18N_RELATIVE_PATH, integration_lang_list[0]))
|
||||
for name in list(integration_lang_list)[1:]:
|
||||
compare_dict: dict = load_json_file(
|
||||
path.join(MIOT_I18N_RELATIVE_PATH, name))
|
||||
if not compare_dict_structure(default_dict, compare_dict):
|
||||
print('compare_dict_structure failed /miot/i18n, ', name)
|
||||
assert False
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_miot_data_sort():
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from miot.const import INTEGRATION_LANGUAGES
|
||||
sort_langs: dict = dict(sorted(INTEGRATION_LANGUAGES.items()))
|
||||
assert list(INTEGRATION_LANGUAGES.keys()) == list(sort_langs.keys()), (
|
||||
'INTEGRATION_LANGUAGES not sorted, correct order\r\n'
|
||||
f'{list(sort_langs.keys())}')
|
||||
assert json.dumps(
|
||||
load_json_file(file_path=SPEC_BOOL_TRANS_FILE)) == json.dumps(
|
||||
sort_bool_trans(file_path=SPEC_BOOL_TRANS_FILE)), (
|
||||
f'{SPEC_BOOL_TRANS_FILE} not sorted, goto project root path'
|
||||
' and run the following command sorting, ',
|
||||
'pytest -s -v -m update ./test/check_rule_format.py')
|
||||
assert json.dumps(
|
||||
load_json_file(file_path=SPEC_MULTI_LANG_FILE)) == json.dumps(
|
||||
sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE)), (
|
||||
f'{SPEC_MULTI_LANG_FILE} not sorted, goto project root path'
|
||||
' and run the following command sorting, ',
|
||||
'pytest -s -v -m update ./test/check_rule_format.py')
|
||||
assert json.dumps(
|
||||
load_json_file(file_path=SPEC_FILTER_FILE)) == json.dumps(
|
||||
sort_spec_filter(file_path=SPEC_FILTER_FILE)), (
|
||||
f'{SPEC_FILTER_FILE} not sorted, goto project root path'
|
||||
' and run the following command sorting, ',
|
||||
'pytest -s -v -m update ./test/check_rule_format.py')
|
||||
|
||||
|
||||
@pytest.mark.update
|
||||
def test_sort_spec_data():
|
||||
sort_data: dict = sort_bool_trans(file_path=SPEC_BOOL_TRANS_FILE)
|
||||
save_json_file(file_path=SPEC_BOOL_TRANS_FILE, data=sort_data)
|
||||
print(SPEC_BOOL_TRANS_FILE, 'formatted.')
|
||||
sort_data = sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE)
|
||||
save_json_file(file_path=SPEC_MULTI_LANG_FILE, data=sort_data)
|
||||
print(SPEC_MULTI_LANG_FILE, 'formatted.')
|
||||
sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE)
|
||||
save_json_file(file_path=SPEC_FILTER_FILE, data=sort_data)
|
||||
print(SPEC_FILTER_FILE, 'formatted.')
|
||||
|
@ -43,6 +43,13 @@ def load_py_file():
|
||||
dst=path.join(TEST_FILES_PATH, 'specs'),
|
||||
dirs_exist_ok=True)
|
||||
print('loaded spec test folder, specs')
|
||||
# Copy lan files to test folder
|
||||
shutil.copytree(
|
||||
src=path.join(
|
||||
TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/lan'),
|
||||
dst=path.join(TEST_FILES_PATH, 'lan'),
|
||||
dirs_exist_ok=True)
|
||||
print('loaded lan test folder, lan')
|
||||
# Copy i18n files to test folder
|
||||
shutil.copytree(
|
||||
src=path.join(
|
||||
|
@ -1,3 +1,4 @@
|
||||
[pytest]
|
||||
markers:
|
||||
github: tests for github actions
|
||||
github: tests for github actions
|
||||
update: update or re-sort config file
|
@ -8,8 +8,37 @@ from zeroconf.asyncio import AsyncZeroconf
|
||||
# pylint: disable=import-outside-toplevel, unused-argument
|
||||
|
||||
|
||||
@pytest.mark.parametrize('test_devices', [{
|
||||
# specv2 model
|
||||
'123456': {
|
||||
'token': '11223344556677d9a03d43936fc384205',
|
||||
'model': 'xiaomi.gateway.hub1'
|
||||
},
|
||||
# profile model
|
||||
'123457': {
|
||||
'token': '11223344556677d9a03d43936fc384205',
|
||||
'model': 'yeelink.light.lamp9'
|
||||
},
|
||||
'123458': {
|
||||
'token': '11223344556677d9a03d43936fc384205',
|
||||
'model': 'zhimi.heater.ma1'
|
||||
},
|
||||
# Non -digital did
|
||||
'group.123456': {
|
||||
'token': '11223344556677d9a03d43936fc384205',
|
||||
'model': 'mijia.light.group3'
|
||||
},
|
||||
'proxy.123456.1': {
|
||||
'token': '11223344556677d9a03d43936fc384205',
|
||||
'model': 'xiaomi.light.p1'
|
||||
},
|
||||
'miwifi_123456': {
|
||||
'token': '11223344556677d9a03d43936fc384205',
|
||||
'model': 'xiaomi.light.p1'
|
||||
}
|
||||
}])
|
||||
@pytest.mark.asyncio
|
||||
async def test_lan_async():
|
||||
async def test_lan_async(test_devices: dict):
|
||||
"""
|
||||
Use the central hub gateway as a test equipment, and through the local area
|
||||
network control central hub gateway indicator light switch. Please replace
|
||||
@ -21,10 +50,13 @@ async def test_lan_async():
|
||||
from miot.miot_lan import MIoTLan
|
||||
from miot.miot_mdns import MipsService
|
||||
|
||||
test_did = '<Your central hub gateway did>'
|
||||
test_token = '<Your central hub gateway token>'
|
||||
# Your central hub gateway did
|
||||
test_did = '111111'
|
||||
# Your central hub gateway did
|
||||
test_token = '11223344556677d9a03d43936fc384205'
|
||||
test_model = 'xiaomi.gateway.hub1'
|
||||
test_if_names = ['<Your computer interface list, such as enp3s0, wlp5s0>']
|
||||
# Your computer interface list, such as enp3s0, wlp5s0
|
||||
test_if_names = ['enp3s0', 'wlp5s0']
|
||||
|
||||
# Check test params
|
||||
assert int(test_did) > 0
|
||||
@ -76,7 +108,8 @@ async def test_lan_async():
|
||||
test_did: {
|
||||
'token': test_token,
|
||||
'model': test_model
|
||||
}
|
||||
},
|
||||
**test_devices
|
||||
})
|
||||
|
||||
# Test sub device state
|
||||
|
44
tools/common.py
Normal file
44
tools/common.py
Normal file
@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Common functions."""
|
||||
import json
|
||||
import yaml
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
|
||||
def load_yaml_file(yaml_file: str) -> dict:
|
||||
with open(yaml_file, 'r', encoding='utf-8') as file:
|
||||
return yaml.safe_load(file)
|
||||
|
||||
|
||||
def save_yaml_file(yaml_file: str, data: dict) -> None:
|
||||
with open(yaml_file, 'w', encoding='utf-8') as file:
|
||||
yaml.safe_dump(
|
||||
data=data, stream=file, allow_unicode=True)
|
||||
|
||||
|
||||
def load_json_file(json_file: str) -> dict:
|
||||
with open(json_file, 'r', encoding='utf-8') as file:
|
||||
return json.load(file)
|
||||
|
||||
|
||||
def save_json_file(json_file: str, data: dict) -> None:
|
||||
with open(json_file, 'w', encoding='utf-8') as file:
|
||||
json.dump(data, file, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
def http_get(
|
||||
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)
|
80
tools/update_lan_rule.py
Normal file
80
tools/update_lan_rule.py
Normal file
@ -0,0 +1,80 @@
|
||||
""" Update LAN rule."""
|
||||
# -*- coding: utf-8 -*-
|
||||
# pylint: disable=relative-beyond-top-level
|
||||
from os import path
|
||||
from common import (
|
||||
http_get,
|
||||
load_yaml_file,
|
||||
save_yaml_file)
|
||||
|
||||
|
||||
ROOT_PATH: str = path.dirname(path.abspath(__file__))
|
||||
LAN_PROFILE_MODELS_FILE: str = path.join(
|
||||
ROOT_PATH,
|
||||
'../custom_components/xiaomi_home/miot/lan/profile_models.yaml')
|
||||
|
||||
|
||||
SPECIAL_MODELS: list[str] = [
|
||||
# model2class-v2
|
||||
'chuangmi.camera.ipc007b', 'chuangmi.camera.ipc019b',
|
||||
'chuangmi.camera.ipc019e', 'chuangmi.camera.ipc020',
|
||||
'chuangmi.camera.v2', 'chuangmi.camera.v5',
|
||||
'chuangmi.camera.v6', 'chuangmi.camera.xiaobai',
|
||||
'chuangmi.radio.v1', 'chuangmi.radio.v2',
|
||||
'hith.foot_bath.q2', 'imou99.camera.tp2',
|
||||
'isa.camera.hl5', 'isa.camera.isc5',
|
||||
'jiqid.mistory.pro', 'jiqid.mistory.v1',
|
||||
'lumi.airrtc.tcpco2ecn01', 'lumi.airrtc.tcpecn02',
|
||||
'lumi.camera.gwagl01', 'miir.light.ir01',
|
||||
'miir.projector.ir01', 'miir.tv.hir01',
|
||||
'miir.tvbox.ir01', 'roome.bhf_light.yf6002',
|
||||
'smith.waterpuri.jnt600', 'viomi.fridge.u2',
|
||||
'xiaovv.camera.lamp', 'xiaovv.camera.ptz',
|
||||
'xiaovv.camera.xva3', 'xiaovv.camera.xvb4',
|
||||
'xiaovv.camera.xvsnowman', 'zdeer.ajh.a8',
|
||||
'zdeer.ajh.a9', 'zdeer.ajh.zda10',
|
||||
'zdeer.ajh.zda9', 'zdeer.ajh.zjy', 'zimi.clock.myk01',
|
||||
# specialModels
|
||||
'chuangmi.camera.ipc004b', 'chuangmi.camera.ipc009',
|
||||
'chuangmi.camera.ipc010', 'chuangmi.camera.ipc013',
|
||||
'chuangmi.camera.ipc013d', 'chuangmi.camera.ipc016',
|
||||
'chuangmi.camera.ipc017', 'chuangmi.camera.ipc019',
|
||||
'chuangmi.camera.ipc021', 'chuangmi.camera.v3',
|
||||
'chuangmi.camera.v4', 'isa.camera.df3',
|
||||
'isa.camera.hlc6', 'lumi.acpartner.v1',
|
||||
'lumi.acpartner.v2', 'lumi.acpartner.v3',
|
||||
'lumi.airrtc.tcpecn01', 'lumi.camera.aq1',
|
||||
'miir.aircondition.ir01', 'miir.aircondition.ir02',
|
||||
'miir.fan.ir01', 'miir.stb.ir01',
|
||||
'miir.tv.ir01', 'mijia.camera.v1',
|
||||
'mijia.camera.v3', 'roborock.sweeper.s5v2',
|
||||
'roborock.vacuum.c1', 'roborock.vacuum.e2',
|
||||
'roborock.vacuum.m1s', 'roborock.vacuum.s5',
|
||||
'rockrobo.vacuum.v1', 'xiaovv.camera.xvd5']
|
||||
|
||||
|
||||
def update_profile_model(file_path: str):
|
||||
profile_rules: dict = http_get(
|
||||
url='https://miot-spec.org/instance/translate/models')
|
||||
if not profile_rules and 'models' not in profile_rules and not isinstance(
|
||||
profile_rules['models'], dict):
|
||||
raise ValueError('Failed to get profile rule')
|
||||
local_rules: dict = load_yaml_file(
|
||||
yaml_file=file_path) or {}
|
||||
for rule, ts in profile_rules['models'].items():
|
||||
if rule not in local_rules:
|
||||
local_rules[rule] = {'ts': ts}
|
||||
else:
|
||||
local_rules[rule]['ts'] = ts
|
||||
for mode in SPECIAL_MODELS:
|
||||
if mode not in local_rules:
|
||||
local_rules[mode] = {'ts': 1531108800}
|
||||
else:
|
||||
local_rules[mode]['ts'] = 1531108800
|
||||
local_rules = dict(sorted(local_rules.items()))
|
||||
save_yaml_file(
|
||||
yaml_file=file_path, data=local_rules)
|
||||
|
||||
|
||||
update_profile_model(file_path=LAN_PROFILE_MODELS_FILE)
|
||||
print('profile model list updated.')
|
Reference in New Issue
Block a user