mirror of
https://github.com/XiaoMi/ha_xiaomi_home.git
synced 2025-06-21 14:50:01 +08:00
refactor: refactor miot device and spec (#592)
* 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
This commit is contained in:
@ -45,13 +45,17 @@ off Xiaomi or its affiliates' products.
|
||||
|
||||
Common utilities.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
from os import path
|
||||
import random
|
||||
from typing import Any, Optional
|
||||
import hashlib
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
from paho.mqtt.matcher import MQTTMatcher
|
||||
import yaml
|
||||
from slugify import slugify
|
||||
|
||||
MIOT_ROOT_PATH: str = path.dirname(path.abspath(__file__))
|
||||
|
||||
@ -83,10 +87,22 @@ def randomize_int(value: int, ratio: float) -> int:
|
||||
"""Randomize an integer value."""
|
||||
return int(value * (1 - ratio + random.random()*2*ratio))
|
||||
|
||||
|
||||
def randomize_float(value: float, ratio: float) -> float:
|
||||
"""Randomize a float value."""
|
||||
return value * (1 - ratio + random.random()*2*ratio)
|
||||
|
||||
|
||||
def slugify_name(name: str, separator: str = '_') -> str:
|
||||
"""Slugify a name."""
|
||||
return slugify(name, separator=separator)
|
||||
|
||||
|
||||
def slugify_did(cloud_server: str, did: str) -> str:
|
||||
"""Slugify a device id."""
|
||||
return slugify(f'{cloud_server}_{did}', separator='_')
|
||||
|
||||
|
||||
class MIoTMatcher(MQTTMatcher):
|
||||
"""MIoT Pub/Sub topic matcher."""
|
||||
|
||||
@ -105,3 +121,68 @@ class MIoTMatcher(MQTTMatcher):
|
||||
return self[topic]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
class MIoTHttp:
|
||||
"""MIoT Common HTTP API."""
|
||||
@staticmethod
|
||||
def get(
|
||||
url: str, params: Optional[dict] = None, headers: Optional[dict] = None
|
||||
) -> Optional[str]:
|
||||
full_url = url
|
||||
if params:
|
||||
encoded_params = urlencode(params)
|
||||
full_url = f'{url}?{encoded_params}'
|
||||
request = Request(full_url, method='GET', headers=headers or {})
|
||||
content: Optional[bytes] = None
|
||||
with urlopen(request) as response:
|
||||
content = response.read()
|
||||
return str(content, 'utf-8') if content else None
|
||||
|
||||
@staticmethod
|
||||
def get_json(
|
||||
url: str, params: Optional[dict] = None, headers: Optional[dict] = None
|
||||
) -> Optional[dict]:
|
||||
response = MIoTHttp.get(url, params, headers)
|
||||
return json.loads(response) if response else None
|
||||
|
||||
@staticmethod
|
||||
def post(
|
||||
url: str, data: Optional[dict] = None, headers: Optional[dict] = None
|
||||
) -> Optional[str]:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def post_json(
|
||||
url: str, data: Optional[dict] = None, headers: Optional[dict] = None
|
||||
) -> Optional[dict]:
|
||||
response = MIoTHttp.post(url, data, headers)
|
||||
return json.loads(response) if response else None
|
||||
|
||||
@staticmethod
|
||||
async def get_async(
|
||||
url: str, params: Optional[dict] = None, headers: Optional[dict] = None,
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
) -> Optional[str]:
|
||||
# TODO: Use aiohttp
|
||||
ev_loop = loop or asyncio.get_running_loop()
|
||||
return await ev_loop.run_in_executor(
|
||||
None, MIoTHttp.get, url, params, headers)
|
||||
|
||||
@staticmethod
|
||||
async def get_json_async(
|
||||
url: str, params: Optional[dict] = None, headers: Optional[dict] = None,
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
) -> Optional[dict]:
|
||||
ev_loop = loop or asyncio.get_running_loop()
|
||||
return await ev_loop.run_in_executor(
|
||||
None, MIoTHttp.get_json, url, params, headers)
|
||||
|
||||
@ staticmethod
|
||||
async def post_async(
|
||||
url: str, data: Optional[dict] = None, headers: Optional[dict] = None,
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
) -> Optional[str]:
|
||||
ev_loop = loop or asyncio.get_running_loop()
|
||||
return await ev_loop.run_in_executor(
|
||||
None, MIoTHttp.post, url, data, headers)
|
||||
|
@ -54,6 +54,10 @@
|
||||
"enable": "aktivieren",
|
||||
"disable": "deaktivieren"
|
||||
},
|
||||
"binary_mode": {
|
||||
"text": "Textsensor-Entität",
|
||||
"bool": "Binärsensor-Entität"
|
||||
},
|
||||
"device_state": {
|
||||
"add": "hinzufügen",
|
||||
"del": "nicht verfügbar",
|
||||
|
@ -54,6 +54,10 @@
|
||||
"enable": "Enable",
|
||||
"disable": "Disable"
|
||||
},
|
||||
"binary_mode": {
|
||||
"text": "Text Sensor Entity",
|
||||
"bool": "Binary Sensor Entity"
|
||||
},
|
||||
"device_state": {
|
||||
"add": "Add",
|
||||
"del": "Unavailable",
|
||||
|
@ -54,6 +54,10 @@
|
||||
"enable": "habilitar",
|
||||
"disable": "deshabilitar"
|
||||
},
|
||||
"binary_mode": {
|
||||
"text": "Entidad del sensor de texto",
|
||||
"bool": "Entidad del sensor binario"
|
||||
},
|
||||
"device_state": {
|
||||
"add": "agregar",
|
||||
"del": "no disponible",
|
||||
|
@ -54,6 +54,10 @@
|
||||
"enable": "activer",
|
||||
"disable": "désactiver"
|
||||
},
|
||||
"binary_mode": {
|
||||
"text": "Entité du capteur de texte",
|
||||
"bool": "Entité du capteur binaire"
|
||||
},
|
||||
"device_state": {
|
||||
"add": "Ajouter",
|
||||
"del": "Supprimer",
|
||||
|
@ -54,6 +54,10 @@
|
||||
"enable": "Abilita",
|
||||
"disable": "Disabilita"
|
||||
},
|
||||
"binary_mode": {
|
||||
"text": "Entità Sensore Testo",
|
||||
"bool": "Entità Sensore Binario"
|
||||
},
|
||||
"device_state": {
|
||||
"add": "Aggiungere",
|
||||
"del": "Non disponibile",
|
||||
@ -149,4 +153,4 @@
|
||||
"-706014006": "Descrizione del dispositivo non trovata"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -54,6 +54,10 @@
|
||||
"enable": "有効",
|
||||
"disable": "無効"
|
||||
},
|
||||
"binary_mode": {
|
||||
"text": "テキストセンサーエンティティ",
|
||||
"bool": "バイナリセンサーエンティティ"
|
||||
},
|
||||
"device_state": {
|
||||
"add": "追加",
|
||||
"del": "利用不可",
|
||||
|
@ -54,6 +54,10 @@
|
||||
"enable": "Inschakelen",
|
||||
"disable": "Uitschakelen"
|
||||
},
|
||||
"binary_mode": {
|
||||
"text": "Tekstsensor-entiteit",
|
||||
"bool": "Binairesensor-entiteit"
|
||||
},
|
||||
"device_state": {
|
||||
"add": "Toevoegen",
|
||||
"del": "Niet beschikbaar",
|
||||
|
@ -54,6 +54,10 @@
|
||||
"enable": "habilitado",
|
||||
"disable": "desabilitado"
|
||||
},
|
||||
"binary_mode": {
|
||||
"text": "Entidade do sensor de texto",
|
||||
"bool": "Entidade do sensor binário"
|
||||
},
|
||||
"device_state": {
|
||||
"add": "adicionar",
|
||||
"del": "indisponível",
|
||||
|
@ -54,6 +54,10 @@
|
||||
"enable": "Habilitar",
|
||||
"disable": "Desabilitar"
|
||||
},
|
||||
"binary_mode": {
|
||||
"text": "Entidade do sensor de texto",
|
||||
"bool": "Entidade do sensor binário"
|
||||
},
|
||||
"device_state": {
|
||||
"add": "Adicionar",
|
||||
"del": "Indisponível",
|
||||
|
@ -54,6 +54,10 @@
|
||||
"enable": "Включить",
|
||||
"disable": "Отключить"
|
||||
},
|
||||
"binary_mode": {
|
||||
"text": "Сущность текстового датчика",
|
||||
"bool": "Сущность бинарного датчика"
|
||||
},
|
||||
"device_state": {
|
||||
"add": "Добавить",
|
||||
"del": "Недоступно",
|
||||
|
@ -54,6 +54,10 @@
|
||||
"enable": "启用",
|
||||
"disable": "禁用"
|
||||
},
|
||||
"binary_mode": {
|
||||
"text": "文本传感器实体",
|
||||
"bool": "二进制传感器实体"
|
||||
},
|
||||
"device_state": {
|
||||
"add": "新增",
|
||||
"del": "不可用",
|
||||
|
@ -54,6 +54,10 @@
|
||||
"enable": "啟用",
|
||||
"disable": "禁用"
|
||||
},
|
||||
"binary_mode": {
|
||||
"text": "文本傳感器實體",
|
||||
"bool": "二進制傳感器實體"
|
||||
},
|
||||
"device_state": {
|
||||
"add": "新增",
|
||||
"del": "不可用",
|
||||
|
@ -59,7 +59,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components import zeroconf
|
||||
|
||||
# pylint: disable=relative-beyond-top-level
|
||||
from .common import MIoTMatcher
|
||||
from .common import MIoTMatcher, slugify_did
|
||||
from .const import (
|
||||
DEFAULT_CTRL_MODE, DEFAULT_INTEGRATION_LANGUAGE, DEFAULT_NICK_NAME, DOMAIN,
|
||||
MIHOME_CERT_EXPIRE_MARGIN, NETWORK_REFRESH_INTERVAL,
|
||||
@ -150,7 +150,7 @@ class MIoTClient:
|
||||
# Device list update timestamp
|
||||
_device_list_update_ts: int
|
||||
|
||||
_sub_source_list: dict[str]
|
||||
_sub_source_list: dict[str, str]
|
||||
_sub_tree: MIoTMatcher
|
||||
_sub_device_state: dict[str, MipsDeviceState]
|
||||
|
||||
@ -169,6 +169,10 @@ class MIoTClient:
|
||||
_show_devices_changed_notify_timer: Optional[asyncio.TimerHandle]
|
||||
# Display devices changed notify
|
||||
_display_devs_notify: list[str]
|
||||
_display_notify_content_hash: Optional[int]
|
||||
# Display binary mode
|
||||
_display_binary_text: bool
|
||||
_display_binary_bool: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -235,6 +239,11 @@ class MIoTClient:
|
||||
|
||||
self._display_devs_notify = entry_data.get(
|
||||
'display_devices_changed_notify', ['add', 'del', 'offline'])
|
||||
self._display_notify_content_hash = None
|
||||
self._display_binary_text = 'text' in entry_data.get(
|
||||
'display_binary_mode', ['text'])
|
||||
self._display_binary_bool = 'bool' in entry_data.get(
|
||||
'display_binary_mode', ['text'])
|
||||
|
||||
async def init_async(self) -> None:
|
||||
# Load user config and check
|
||||
@ -469,6 +478,14 @@ class MIoTClient:
|
||||
def display_devices_changed_notify(self) -> list[str]:
|
||||
return self._display_devs_notify
|
||||
|
||||
@property
|
||||
def display_binary_text(self) -> bool:
|
||||
return self._display_binary_text
|
||||
|
||||
@property
|
||||
def display_binary_bool(self) -> bool:
|
||||
return self._display_binary_bool
|
||||
|
||||
@display_devices_changed_notify.setter
|
||||
def display_devices_changed_notify(self, value: list[str]) -> None:
|
||||
if set(value) == set(self._display_devs_notify):
|
||||
@ -543,7 +560,8 @@ class MIoTClient:
|
||||
return True
|
||||
except Exception as err:
|
||||
self.__show_client_error_notify(
|
||||
message=self._i18n.translate('miot.client.invalid_oauth_info'),
|
||||
message=self._i18n.translate(
|
||||
'miot.client.invalid_oauth_info'), # type: ignore
|
||||
notify_key='oauth_info')
|
||||
_LOGGER.error(
|
||||
'refresh oauth info error (%s, %s), %s, %s',
|
||||
@ -586,7 +604,8 @@ class MIoTClient:
|
||||
return True
|
||||
except MIoTClientError as error:
|
||||
self.__show_client_error_notify(
|
||||
message=self._i18n.translate('miot.client.invalid_cert_info'),
|
||||
message=self._i18n.translate(
|
||||
'miot.client.invalid_cert_info'), # type: ignore
|
||||
notify_key='user_cert')
|
||||
_LOGGER.error(
|
||||
'refresh user cert error, %s, %s',
|
||||
@ -872,8 +891,16 @@ class MIoTClient:
|
||||
# Update notify
|
||||
self.__request_show_devices_changed_notify()
|
||||
|
||||
async def remove_device2_async(self, did_tag: str) -> None:
|
||||
for did in self._device_list_cache:
|
||||
d_tag = slugify_did(cloud_server=self._cloud_server, did=did)
|
||||
if did_tag == d_tag:
|
||||
await self.remove_device_async(did)
|
||||
break
|
||||
|
||||
def __get_exec_error_with_rc(self, rc: int) -> str:
|
||||
err_msg: str = self._i18n.translate(key=f'error.common.{rc}')
|
||||
err_msg: str = self._i18n.translate(
|
||||
key=f'error.common.{rc}') # type: ignore
|
||||
if not err_msg:
|
||||
err_msg = f'{self._i18n.translate(key="error.common.-10000")}, '
|
||||
err_msg += f'code={rc}'
|
||||
@ -1280,7 +1307,7 @@ class MIoTClient:
|
||||
if not cache_list:
|
||||
self.__show_client_error_notify(
|
||||
message=self._i18n.translate(
|
||||
'miot.client.invalid_device_cache'),
|
||||
'miot.client.invalid_device_cache'), # type: ignore
|
||||
notify_key='device_cache')
|
||||
raise MIoTClientError('load device list from cache error')
|
||||
else:
|
||||
@ -1368,7 +1395,8 @@ class MIoTClient:
|
||||
home_ids=list(self._entry_data.get('home_selected', {}).keys()))
|
||||
if not result and 'devices' not in result:
|
||||
self.__show_client_error_notify(
|
||||
message=self._i18n.translate('miot.client.device_cloud_error'),
|
||||
message=self._i18n.translate(
|
||||
'miot.client.device_cloud_error'), # type: ignore
|
||||
notify_key='device_cloud')
|
||||
return
|
||||
else:
|
||||
@ -1725,13 +1753,14 @@ class MIoTClient:
|
||||
|
||||
@final
|
||||
def __show_client_error_notify(
|
||||
self, message: str, notify_key: str = ''
|
||||
self, message: Optional[str], notify_key: str = ''
|
||||
) -> None:
|
||||
if message:
|
||||
|
||||
self._persistence_notify(
|
||||
f'{DOMAIN}{self._uid}{self._cloud_server}{notify_key}error',
|
||||
self._i18n.translate(
|
||||
key='miot.client.xiaomi_home_error_title'),
|
||||
key='miot.client.xiaomi_home_error_title'), # type: ignore
|
||||
self._i18n.translate(
|
||||
key='miot.client.xiaomi_home_error',
|
||||
replace={
|
||||
@ -1739,8 +1768,7 @@ class MIoTClient:
|
||||
'nick_name', DEFAULT_NICK_NAME),
|
||||
'uid': self._uid,
|
||||
'cloud_server': self._cloud_server,
|
||||
'message': message
|
||||
}))
|
||||
'message': message})) # type: ignore
|
||||
else:
|
||||
self._persistence_notify(
|
||||
f'{DOMAIN}{self._uid}{self._cloud_server}{notify_key}error',
|
||||
@ -1806,27 +1834,34 @@ class MIoTClient:
|
||||
key='miot.client.device_list_add',
|
||||
replace={
|
||||
'count': count_add,
|
||||
'message': message_add})
|
||||
'message': message_add}) # type: ignore
|
||||
if 'del' in self._display_devs_notify and count_del:
|
||||
message += self._i18n.translate(
|
||||
key='miot.client.device_list_del',
|
||||
replace={
|
||||
'count': count_del,
|
||||
'message': message_del})
|
||||
'message': message_del}) # type: ignore
|
||||
if 'offline' in self._display_devs_notify and count_offline:
|
||||
message += self._i18n.translate(
|
||||
key='miot.client.device_list_offline',
|
||||
replace={
|
||||
'count': count_offline,
|
||||
'message': message_offline})
|
||||
'message': message_offline}) # type: ignore
|
||||
if message != '':
|
||||
msg_hash = hash(message)
|
||||
if msg_hash == self._display_notify_content_hash:
|
||||
# Notify content no change, return
|
||||
_LOGGER.debug(
|
||||
'device list changed notify content no change, return')
|
||||
return
|
||||
network_status = self._i18n.translate(
|
||||
key='miot.client.network_status_online'
|
||||
if self._network.network_status
|
||||
else 'miot.client.network_status_offline')
|
||||
self._persistence_notify(
|
||||
self.__gen_notify_key('dev_list_changed'),
|
||||
self._i18n.translate('miot.client.device_list_changed_title'),
|
||||
self._i18n.translate(
|
||||
'miot.client.device_list_changed_title'), # type: ignore
|
||||
self._i18n.translate(
|
||||
key='miot.client.device_list_changed',
|
||||
replace={
|
||||
@ -1835,8 +1870,8 @@ class MIoTClient:
|
||||
'uid': self._uid,
|
||||
'cloud_server': self._cloud_server,
|
||||
'network_status': network_status,
|
||||
'message': message
|
||||
}))
|
||||
'message': message})) # type: ignore
|
||||
self._display_notify_content_hash = msg_hash
|
||||
_LOGGER.debug(
|
||||
'show device list changed notify, add %s, del %s, offline %s',
|
||||
count_add, count_del, count_offline)
|
||||
|
@ -744,7 +744,7 @@ class MIoTHttpClient:
|
||||
prop_obj['fut'].set_result(None)
|
||||
if props_req:
|
||||
_LOGGER.info(
|
||||
'get prop from cloud failed, %s, %s', len(key), props_req)
|
||||
'get prop from cloud failed, %s', props_req)
|
||||
|
||||
if self._get_prop_list:
|
||||
self._get_prop_timer = self._main_loop.call_later(
|
||||
|
@ -75,7 +75,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.components.switch import SwitchDeviceClass
|
||||
from homeassistant.util import slugify
|
||||
|
||||
|
||||
# pylint: disable=relative-beyond-top-level
|
||||
from .specs.specv2entity import (
|
||||
@ -85,6 +85,7 @@ from .specs.specv2entity import (
|
||||
SPEC_PROP_TRANS_MAP,
|
||||
SPEC_SERVICE_TRANS_MAP
|
||||
)
|
||||
from .common import slugify_name, slugify_did
|
||||
from .const import DOMAIN
|
||||
from .miot_client import MIoTClient
|
||||
from .miot_error import MIoTClientError, MIoTDeviceError
|
||||
@ -94,7 +95,9 @@ from .miot_spec import (
|
||||
MIoTSpecEvent,
|
||||
MIoTSpecInstance,
|
||||
MIoTSpecProperty,
|
||||
MIoTSpecService
|
||||
MIoTSpecService,
|
||||
MIoTSpecValueList,
|
||||
MIoTSpecValueRange
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -142,9 +145,12 @@ class MIoTDevice:
|
||||
_room_id: str
|
||||
_room_name: str
|
||||
|
||||
_suggested_area: str
|
||||
_suggested_area: Optional[str]
|
||||
|
||||
_device_state_sub_list: dict[str, Callable[[str, MIoTDeviceState], None]]
|
||||
_sub_id: int
|
||||
_device_state_sub_list: dict[str, dict[
|
||||
str, Callable[[str, MIoTDeviceState], None]]]
|
||||
_value_sub_list: dict[str, dict[str, Callable[[dict, Any], None]]]
|
||||
|
||||
_entity_list: dict[str, list[MIoTEntityData]]
|
||||
_prop_list: dict[str, list[MIoTSpecProperty]]
|
||||
@ -153,7 +159,7 @@ class MIoTDevice:
|
||||
|
||||
def __init__(
|
||||
self, miot_client: MIoTClient,
|
||||
device_info: dict[str, str],
|
||||
device_info: dict[str, Any],
|
||||
spec_instance: MIoTSpecInstance
|
||||
) -> None:
|
||||
self.miot_client = miot_client
|
||||
@ -183,7 +189,9 @@ class MIoTDevice:
|
||||
case _:
|
||||
self._suggested_area = None
|
||||
|
||||
self._sub_id = 0
|
||||
self._device_state_sub_list = {}
|
||||
self._value_sub_list = {}
|
||||
self._entity_list = {}
|
||||
self._prop_list = {}
|
||||
self._event_list = {}
|
||||
@ -234,36 +242,76 @@ class MIoTDevice:
|
||||
|
||||
def sub_device_state(
|
||||
self, key: str, handler: Callable[[str, MIoTDeviceState], None]
|
||||
) -> bool:
|
||||
self._device_state_sub_list[key] = handler
|
||||
return True
|
||||
) -> int:
|
||||
self._sub_id += 1
|
||||
if key in self._device_state_sub_list:
|
||||
self._device_state_sub_list[key][str(self._sub_id)] = handler
|
||||
else:
|
||||
self._device_state_sub_list[key] = {str(self._sub_id): handler}
|
||||
return self._sub_id
|
||||
|
||||
def unsub_device_state(self, key: str) -> bool:
|
||||
self._device_state_sub_list.pop(key, None)
|
||||
return True
|
||||
def unsub_device_state(self, key: str, sub_id: int) -> None:
|
||||
sub_list = self._device_state_sub_list.get(key, None)
|
||||
if sub_list:
|
||||
sub_list.pop(str(sub_id), None)
|
||||
if not sub_list:
|
||||
self._device_state_sub_list.pop(key, None)
|
||||
|
||||
def sub_property(
|
||||
self, handler: Callable[[dict, Any], None], siid: int = None,
|
||||
piid: int = None, handler_ctx: Any = None
|
||||
) -> bool:
|
||||
return self.miot_client.sub_prop(
|
||||
did=self._did, handler=handler, siid=siid, piid=piid,
|
||||
handler_ctx=handler_ctx)
|
||||
self, handler: Callable[[dict, Any], None], siid: int, piid: int
|
||||
) -> int:
|
||||
key: str = f'p.{siid}.{piid}'
|
||||
|
||||
def unsub_property(self, siid: int = None, piid: int = None) -> bool:
|
||||
return self.miot_client.unsub_prop(did=self._did, siid=siid, piid=piid)
|
||||
def _on_prop_changed(params: dict, ctx: Any) -> None:
|
||||
for handler in self._value_sub_list[key].values():
|
||||
handler(params, ctx)
|
||||
|
||||
self._sub_id += 1
|
||||
if key in self._value_sub_list:
|
||||
self._value_sub_list[key][str(self._sub_id)] = handler
|
||||
else:
|
||||
self._value_sub_list[key] = {str(self._sub_id): handler}
|
||||
self.miot_client.sub_prop(
|
||||
did=self._did, handler=_on_prop_changed, siid=siid, piid=piid)
|
||||
return self._sub_id
|
||||
|
||||
def unsub_property(self, siid: int, piid: int, sub_id: int) -> None:
|
||||
key: str = f'p.{siid}.{piid}'
|
||||
|
||||
sub_list = self._value_sub_list.get(key, None)
|
||||
if sub_list:
|
||||
sub_list.pop(str(sub_id), None)
|
||||
if not sub_list:
|
||||
self.miot_client.unsub_prop(did=self._did, siid=siid, piid=piid)
|
||||
self._value_sub_list.pop(key, None)
|
||||
|
||||
def sub_event(
|
||||
self, handler: Callable[[dict, Any], None], siid: int = None,
|
||||
eiid: int = None, handler_ctx: Any = None
|
||||
) -> bool:
|
||||
return self.miot_client.sub_event(
|
||||
did=self._did, handler=handler, siid=siid, eiid=eiid,
|
||||
handler_ctx=handler_ctx)
|
||||
self, handler: Callable[[dict, Any], None], siid: int, eiid: int
|
||||
) -> int:
|
||||
key: str = f'e.{siid}.{eiid}'
|
||||
|
||||
def unsub_event(self, siid: int = None, eiid: int = None) -> bool:
|
||||
return self.miot_client.unsub_event(
|
||||
did=self._did, siid=siid, eiid=eiid)
|
||||
def _on_event_occurred(params: dict, ctx: Any) -> None:
|
||||
for handler in self._value_sub_list[key].values():
|
||||
handler(params, ctx)
|
||||
|
||||
self._sub_id += 1
|
||||
if key in self._value_sub_list:
|
||||
self._value_sub_list[key][str(self._sub_id)] = handler
|
||||
else:
|
||||
self._value_sub_list[key] = {str(self._sub_id): handler}
|
||||
self.miot_client.sub_event(
|
||||
did=self._did, handler=_on_event_occurred, siid=siid, eiid=eiid)
|
||||
return self._sub_id
|
||||
|
||||
def unsub_event(self, siid: int, eiid: int, sub_id: int) -> None:
|
||||
key: str = f'e.{siid}.{eiid}'
|
||||
|
||||
sub_list = self._value_sub_list.get(key, None)
|
||||
if sub_list:
|
||||
sub_list.pop(str(sub_id), None)
|
||||
if not sub_list:
|
||||
self.miot_client.unsub_event(did=self._did, siid=siid, eiid=eiid)
|
||||
self._value_sub_list.pop(key, None)
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
@ -287,11 +335,8 @@ class MIoTDevice:
|
||||
|
||||
@property
|
||||
def did_tag(self) -> str:
|
||||
return slugify(f'{self.miot_client.cloud_server}_{self._did}')
|
||||
|
||||
@staticmethod
|
||||
def gen_did_tag(cloud_server: str, did: str) -> str:
|
||||
return slugify(f'{cloud_server}_{did}')
|
||||
return slugify_did(
|
||||
cloud_server=self.miot_client.cloud_server, did=self._did)
|
||||
|
||||
def gen_device_entity_id(self, ha_domain: str) -> str:
|
||||
return (
|
||||
@ -308,21 +353,24 @@ class MIoTDevice:
|
||||
) -> str:
|
||||
return (
|
||||
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
|
||||
f'{self._model_strs[-1][:20]}_{slugify(spec_name)}_p_{siid}_{piid}')
|
||||
f'{self._model_strs[-1][:20]}_{slugify_name(spec_name)}'
|
||||
f'_p_{siid}_{piid}')
|
||||
|
||||
def gen_event_entity_id(
|
||||
self, ha_domain: str, spec_name: str, siid: int, eiid: int
|
||||
) -> str:
|
||||
return (
|
||||
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
|
||||
f'{self._model_strs[-1][:20]}_{slugify(spec_name)}_e_{siid}_{eiid}')
|
||||
f'{self._model_strs[-1][:20]}_{slugify_name(spec_name)}'
|
||||
f'_e_{siid}_{eiid}')
|
||||
|
||||
def gen_action_entity_id(
|
||||
self, ha_domain: str, spec_name: str, siid: int, aiid: int
|
||||
) -> str:
|
||||
return (
|
||||
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
|
||||
f'{self._model_strs[-1][:20]}_{slugify(spec_name)}_a_{siid}_{aiid}')
|
||||
f'{self._model_strs[-1][:20]}_{slugify_name(spec_name)}'
|
||||
f'_a_{siid}_{aiid}')
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@ -341,14 +389,20 @@ class MIoTDevice:
|
||||
self._entity_list[entity_data.platform].append(entity_data)
|
||||
|
||||
def append_prop(self, prop: MIoTSpecProperty) -> None:
|
||||
if not prop.platform:
|
||||
return
|
||||
self._prop_list.setdefault(prop.platform, [])
|
||||
self._prop_list[prop.platform].append(prop)
|
||||
|
||||
def append_event(self, event: MIoTSpecEvent) -> None:
|
||||
if not event.platform:
|
||||
return
|
||||
self._event_list.setdefault(event.platform, [])
|
||||
self._event_list[event.platform].append(event)
|
||||
|
||||
def append_action(self, action: MIoTSpecAction) -> None:
|
||||
if not action.platform:
|
||||
return
|
||||
self._action_list.setdefault(action.platform, [])
|
||||
self._action_list[action.platform].append(action)
|
||||
|
||||
@ -507,7 +561,7 @@ class MIoTDevice:
|
||||
if prop_access != (SPEC_PROP_TRANS_MAP[
|
||||
'entities'][platform]['access']):
|
||||
return None
|
||||
if prop.format_ not in SPEC_PROP_TRANS_MAP[
|
||||
if prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
|
||||
'entities'][platform]['format']:
|
||||
return None
|
||||
if prop.unit:
|
||||
@ -560,9 +614,9 @@ class MIoTDevice:
|
||||
# general conversion
|
||||
if not prop.platform:
|
||||
if prop.writable:
|
||||
if prop.format_ == 'str':
|
||||
if prop.format_ == str:
|
||||
prop.platform = 'text'
|
||||
elif prop.format_ == 'bool':
|
||||
elif prop.format_ == bool:
|
||||
prop.platform = 'switch'
|
||||
prop.device_class = SwitchDeviceClass.SWITCH
|
||||
elif prop.value_list:
|
||||
@ -573,9 +627,11 @@ class MIoTDevice:
|
||||
# Irregular property will not be transformed.
|
||||
pass
|
||||
elif prop.readable or prop.notifiable:
|
||||
prop.platform = 'sensor'
|
||||
if prop.platform:
|
||||
self.append_prop(prop=prop)
|
||||
if prop.format_ == bool:
|
||||
prop.platform = 'binary_sensor'
|
||||
else:
|
||||
prop.platform = 'sensor'
|
||||
self.append_prop(prop=prop)
|
||||
# STEP 3.2: event conversion
|
||||
for event in service.events:
|
||||
if event.platform:
|
||||
@ -703,10 +759,11 @@ class MIoTDevice:
|
||||
def __on_device_state_changed(
|
||||
self, did: str, state: MIoTDeviceState, ctx: Any
|
||||
) -> None:
|
||||
self._online = state
|
||||
for key, handler in self._device_state_sub_list.items():
|
||||
self.miot_client.main_loop.call_soon_threadsafe(
|
||||
handler, key, state)
|
||||
self._online = state == MIoTDeviceState.ONLINE
|
||||
for key, sub_list in self._device_state_sub_list.items():
|
||||
for handler in sub_list.values():
|
||||
self.miot_client.main_loop.call_soon_threadsafe(
|
||||
handler, key, state)
|
||||
|
||||
|
||||
class MIoTServiceEntity(Entity):
|
||||
@ -718,8 +775,11 @@ class MIoTServiceEntity(Entity):
|
||||
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
_prop_value_map: dict[MIoTSpecProperty, Any]
|
||||
_state_sub_id: int
|
||||
_value_sub_ids: dict[str, int]
|
||||
|
||||
_event_occurred_handler: Callable[[MIoTSpecEvent, dict], None]
|
||||
_event_occurred_handler: Optional[
|
||||
Callable[[MIoTSpecEvent, dict], None]]
|
||||
_prop_changed_subs: dict[
|
||||
MIoTSpecProperty, Callable[[MIoTSpecProperty, Any], None]]
|
||||
|
||||
@ -738,13 +798,15 @@ class MIoTServiceEntity(Entity):
|
||||
self.entity_data = entity_data
|
||||
self._main_loop = miot_device.miot_client.main_loop
|
||||
self._prop_value_map = {}
|
||||
self._state_sub_id = 0
|
||||
self._value_sub_ids = {}
|
||||
# Gen entity id
|
||||
if isinstance(entity_data.spec, MIoTSpecInstance):
|
||||
if isinstance(self.entity_data.spec, MIoTSpecInstance):
|
||||
self.entity_id = miot_device.gen_device_entity_id(DOMAIN)
|
||||
self._attr_name = f' {self.entity_data.spec.description_trans}'
|
||||
elif isinstance(entity_data.spec, MIoTSpecService):
|
||||
elif isinstance(self.entity_data.spec, MIoTSpecService):
|
||||
self.entity_id = miot_device.gen_service_entity_id(
|
||||
DOMAIN, siid=entity_data.spec.iid)
|
||||
DOMAIN, siid=self.entity_data.spec.iid)
|
||||
self._attr_name = (
|
||||
f'{"* "if self.entity_data.spec.proprietary else " "}'
|
||||
f'{self.entity_data.spec.description_trans}')
|
||||
@ -763,7 +825,9 @@ class MIoTServiceEntity(Entity):
|
||||
self.entity_id)
|
||||
|
||||
@property
|
||||
def event_occurred_handler(self) -> Callable[[MIoTSpecEvent, dict], None]:
|
||||
def event_occurred_handler(
|
||||
self
|
||||
) -> Optional[Callable[[MIoTSpecEvent, dict], None]]:
|
||||
return self._event_occurred_handler
|
||||
|
||||
@event_occurred_handler.setter
|
||||
@ -784,25 +848,27 @@ class MIoTServiceEntity(Entity):
|
||||
self._prop_changed_subs.pop(prop, None)
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict:
|
||||
def device_info(self) -> Optional[DeviceInfo]:
|
||||
return self.miot_device.device_info
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
state_id = 's.0'
|
||||
if isinstance(self.entity_data.spec, MIoTSpecService):
|
||||
state_id = f's.{self.entity_data.spec.iid}'
|
||||
self.miot_device.sub_device_state(
|
||||
self._state_sub_id = self.miot_device.sub_device_state(
|
||||
key=state_id, handler=self.__on_device_state_changed)
|
||||
# Sub prop
|
||||
for prop in self.entity_data.props:
|
||||
if not prop.notifiable and not prop.readable:
|
||||
continue
|
||||
self.miot_device.sub_property(
|
||||
key = f'p.{prop.service.iid}.{prop.iid}'
|
||||
self._value_sub_ids[key] = self.miot_device.sub_property(
|
||||
handler=self.__on_properties_changed,
|
||||
siid=prop.service.iid, piid=prop.iid)
|
||||
# Sub event
|
||||
for event in self.entity_data.events:
|
||||
self.miot_device.sub_event(
|
||||
key = f'e.{event.service.iid}.{event.iid}'
|
||||
self._value_sub_ids[key] = self.miot_device.sub_event(
|
||||
handler=self.__on_event_occurred,
|
||||
siid=event.service.iid, eiid=event.iid)
|
||||
|
||||
@ -817,30 +883,39 @@ class MIoTServiceEntity(Entity):
|
||||
state_id = 's.0'
|
||||
if isinstance(self.entity_data.spec, MIoTSpecService):
|
||||
state_id = f's.{self.entity_data.spec.iid}'
|
||||
self.miot_device.unsub_device_state(key=state_id)
|
||||
self.miot_device.unsub_device_state(
|
||||
key=state_id, sub_id=self._state_sub_id)
|
||||
# Unsub prop
|
||||
for prop in self.entity_data.props:
|
||||
if not prop.notifiable and not prop.readable:
|
||||
continue
|
||||
self.miot_device.unsub_property(
|
||||
siid=prop.service.iid, piid=prop.iid)
|
||||
sub_id = self._value_sub_ids.pop(
|
||||
f'p.{prop.service.iid}.{prop.iid}', None)
|
||||
if sub_id:
|
||||
self.miot_device.unsub_property(
|
||||
siid=prop.service.iid, piid=prop.iid, sub_id=sub_id)
|
||||
# Unsub event
|
||||
for event in self.entity_data.events:
|
||||
self.miot_device.unsub_event(
|
||||
siid=event.service.iid, eiid=event.iid)
|
||||
sub_id = self._value_sub_ids.pop(
|
||||
f'e.{event.service.iid}.{event.iid}', None)
|
||||
if sub_id:
|
||||
self.miot_device.unsub_event(
|
||||
siid=event.service.iid, eiid=event.iid, sub_id=sub_id)
|
||||
|
||||
def get_map_description(self, map_: dict[int, Any], key: int) -> Any:
|
||||
def get_map_value(
|
||||
self, map_: dict[int, Any], key: int
|
||||
) -> Any:
|
||||
if map_ is None:
|
||||
return None
|
||||
return map_.get(key, None)
|
||||
|
||||
def get_map_value(
|
||||
self, map_: dict[int, Any], description: Any
|
||||
def get_map_key(
|
||||
self, map_: dict[int, Any], value: Any
|
||||
) -> Optional[int]:
|
||||
if map_ is None:
|
||||
return None
|
||||
for key, value in map_.items():
|
||||
if value == description:
|
||||
for key, value_ in map_.items():
|
||||
if value_ == value:
|
||||
return key
|
||||
return None
|
||||
|
||||
@ -999,11 +1074,12 @@ class MIoTPropertyEntity(Entity):
|
||||
service: MIoTSpecService
|
||||
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
# {'min':int, 'max':int, 'step': int}
|
||||
_value_range: dict[str, int]
|
||||
_value_range: Optional[MIoTSpecValueRange]
|
||||
# {Any: Any}
|
||||
_value_list: dict[Any, Any]
|
||||
_value_list: Optional[MIoTSpecValueList]
|
||||
_value: Any
|
||||
_state_sub_id: int
|
||||
_value_sub_id: int
|
||||
|
||||
_pending_write_ha_state_timer: Optional[asyncio.TimerHandle]
|
||||
|
||||
@ -1015,12 +1091,10 @@ class MIoTPropertyEntity(Entity):
|
||||
self.service = spec.service
|
||||
self._main_loop = miot_device.miot_client.main_loop
|
||||
self._value_range = spec.value_range
|
||||
if spec.value_list:
|
||||
self._value_list = {
|
||||
item['value']: item['description'] for item in spec.value_list}
|
||||
else:
|
||||
self._value_list = None
|
||||
self._value_list = spec.value_list
|
||||
self._value = None
|
||||
self._state_sub_id = 0
|
||||
self._value_sub_id = 0
|
||||
self._pending_write_ha_state_timer = None
|
||||
# Gen entity_id
|
||||
self.entity_id = self.miot_device.gen_prop_entity_id(
|
||||
@ -1042,16 +1116,16 @@ class MIoTPropertyEntity(Entity):
|
||||
self._value_list)
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict:
|
||||
def device_info(self) -> Optional[DeviceInfo]:
|
||||
return self.miot_device.device_info
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
# Sub device state changed
|
||||
self.miot_device.sub_device_state(
|
||||
self._state_sub_id = self.miot_device.sub_device_state(
|
||||
key=f'{ self.service.iid}.{self.spec.iid}',
|
||||
handler=self.__on_device_state_changed)
|
||||
# Sub value changed
|
||||
self.miot_device.sub_property(
|
||||
self._value_sub_id = self.miot_device.sub_property(
|
||||
handler=self.__on_value_changed,
|
||||
siid=self.service.iid, piid=self.spec.iid)
|
||||
# Refresh value
|
||||
@ -1063,22 +1137,21 @@ class MIoTPropertyEntity(Entity):
|
||||
self._pending_write_ha_state_timer.cancel()
|
||||
self._pending_write_ha_state_timer = None
|
||||
self.miot_device.unsub_device_state(
|
||||
key=f'{ self.service.iid}.{self.spec.iid}')
|
||||
key=f'{ self.service.iid}.{self.spec.iid}',
|
||||
sub_id=self._state_sub_id)
|
||||
self.miot_device.unsub_property(
|
||||
siid=self.service.iid, piid=self.spec.iid)
|
||||
siid=self.service.iid, piid=self.spec.iid,
|
||||
sub_id=self._value_sub_id)
|
||||
|
||||
def get_vlist_description(self, value: Any) -> str:
|
||||
def get_vlist_description(self, value: Any) -> Optional[str]:
|
||||
if not self._value_list:
|
||||
return None
|
||||
return self._value_list.get(value, None)
|
||||
return self._value_list.get_description_by_value(value)
|
||||
|
||||
def get_vlist_value(self, description: str) -> Any:
|
||||
if not self._value_list:
|
||||
return None
|
||||
for key, value in self._value_list.items():
|
||||
if value == description:
|
||||
return key
|
||||
return None
|
||||
return self._value_list.get_value_by_description(description)
|
||||
|
||||
async def set_property_async(self, value: Any) -> bool:
|
||||
if not self.spec.writable:
|
||||
@ -1148,9 +1221,10 @@ class MIoTEventEntity(Entity):
|
||||
service: MIoTSpecService
|
||||
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
_value: Any
|
||||
_attr_event_types: list[str]
|
||||
_arguments_map: dict[int, str]
|
||||
_state_sub_id: int
|
||||
_value_sub_id: int
|
||||
|
||||
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecEvent) -> None:
|
||||
if miot_device is None or spec is None or spec.service is None:
|
||||
@ -1159,7 +1233,6 @@ class MIoTEventEntity(Entity):
|
||||
self.spec = spec
|
||||
self.service = spec.service
|
||||
self._main_loop = miot_device.miot_client.main_loop
|
||||
self._value = None
|
||||
# Gen entity_id
|
||||
self.entity_id = self.miot_device.gen_event_entity_id(
|
||||
ha_domain=DOMAIN, spec_name=spec.name,
|
||||
@ -1177,6 +1250,8 @@ class MIoTEventEntity(Entity):
|
||||
self._arguments_map = {}
|
||||
for prop in spec.argument:
|
||||
self._arguments_map[prop.iid] = prop.description_trans
|
||||
self._state_sub_id = 0
|
||||
self._value_sub_id = 0
|
||||
|
||||
_LOGGER.info(
|
||||
'new miot event entity, %s, %s, %s, %s, %s',
|
||||
@ -1184,29 +1259,31 @@ class MIoTEventEntity(Entity):
|
||||
spec.device_class, self.entity_id)
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict:
|
||||
def device_info(self) -> Optional[DeviceInfo]:
|
||||
return self.miot_device.device_info
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
# Sub device state changed
|
||||
self.miot_device.sub_device_state(
|
||||
self._state_sub_id = self.miot_device.sub_device_state(
|
||||
key=f'event.{ self.service.iid}.{self.spec.iid}',
|
||||
handler=self.__on_device_state_changed)
|
||||
# Sub value changed
|
||||
self.miot_device.sub_event(
|
||||
self._value_sub_id = self.miot_device.sub_event(
|
||||
handler=self.__on_event_occurred,
|
||||
siid=self.service.iid, eiid=self.spec.iid)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
self.miot_device.unsub_device_state(
|
||||
key=f'event.{ self.service.iid}.{self.spec.iid}')
|
||||
key=f'event.{ self.service.iid}.{self.spec.iid}',
|
||||
sub_id=self._state_sub_id)
|
||||
self.miot_device.unsub_event(
|
||||
siid=self.service.iid, eiid=self.spec.iid)
|
||||
siid=self.service.iid, eiid=self.spec.iid,
|
||||
sub_id=self._value_sub_id)
|
||||
|
||||
@abstractmethod
|
||||
def on_event_occurred(
|
||||
self, name: str, arguments: list[dict[int, Any]]
|
||||
): ...
|
||||
self, name: str, arguments: dict[str, Any] | None = None
|
||||
) -> None: ...
|
||||
|
||||
def __on_event_occurred(self, params: dict, ctx: Any) -> None:
|
||||
_LOGGER.debug('event occurred, %s', params)
|
||||
@ -1253,11 +1330,11 @@ class MIoTActionEntity(Entity):
|
||||
miot_device: MIoTDevice
|
||||
spec: MIoTSpecAction
|
||||
service: MIoTSpecService
|
||||
action_platform: str
|
||||
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
_in_map: dict[int, MIoTSpecProperty]
|
||||
_out_map: dict[int, MIoTSpecProperty]
|
||||
_state_sub_id: int
|
||||
|
||||
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecAction) -> None:
|
||||
if miot_device is None or spec is None or spec.service is None:
|
||||
@ -1265,8 +1342,8 @@ class MIoTActionEntity(Entity):
|
||||
self.miot_device = miot_device
|
||||
self.spec = spec
|
||||
self.service = spec.service
|
||||
self.action_platform = 'action'
|
||||
self._main_loop = miot_device.miot_client.main_loop
|
||||
self._state_sub_id = 0
|
||||
# Gen entity_id
|
||||
self.entity_id = self.miot_device.gen_action_entity_id(
|
||||
ha_domain=DOMAIN, spec_name=spec.name,
|
||||
@ -1286,19 +1363,22 @@ class MIoTActionEntity(Entity):
|
||||
spec.device_class, self.entity_id)
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict:
|
||||
def device_info(self) -> Optional[DeviceInfo]:
|
||||
return self.miot_device.device_info
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
self.miot_device.sub_device_state(
|
||||
key=f'{self.action_platform}.{ self.service.iid}.{self.spec.iid}',
|
||||
self._state_sub_id = self.miot_device.sub_device_state(
|
||||
key=f'a.{ self.service.iid}.{self.spec.iid}',
|
||||
handler=self.__on_device_state_changed)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
self.miot_device.unsub_device_state(
|
||||
key=f'{self.action_platform}.{ self.service.iid}.{self.spec.iid}')
|
||||
key=f'a.{ self.service.iid}.{self.spec.iid}',
|
||||
sub_id=self._state_sub_id)
|
||||
|
||||
async def action_async(self, in_list: list = None) -> Optional[list]:
|
||||
async def action_async(
|
||||
self, in_list: Optional[list] = None
|
||||
) -> Optional[list]:
|
||||
try:
|
||||
return await self.miot_device.miot_client.action_async(
|
||||
did=self.miot_device.did,
|
||||
|
@ -663,7 +663,8 @@ class _MipsClient(ABC):
|
||||
|
||||
def __on_disconnect(self, client, user_data, rc, props) -> None:
|
||||
if self._mqtt_state:
|
||||
self.log_error(f'mips disconnect, {rc}, {props}')
|
||||
(self.log_info if rc == 0 else self.log_error)(
|
||||
f'mips disconnect, {rc}, {props}')
|
||||
self._mqtt_state = False
|
||||
if self._mqtt_timer:
|
||||
self._mqtt_timer.cancel()
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -58,7 +58,6 @@ from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Union
|
||||
import logging
|
||||
from urllib.request import Request, urlopen
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.x509.oid import NameOID
|
||||
@ -66,13 +65,13 @@ from cryptography import x509
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
|
||||
|
||||
# pylint: disable=relative-beyond-top-level
|
||||
from .common import load_json_file
|
||||
from .const import (
|
||||
DEFAULT_INTEGRATION_LANGUAGE,
|
||||
MANUFACTURER_EFFECTIVE_TIME,
|
||||
MIHOME_CA_CERT_STR,
|
||||
MIHOME_CA_CERT_SHA256)
|
||||
from .common import MIoTHttp
|
||||
from .miot_error import MIoTCertError, MIoTError, MIoTStorageError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -93,10 +92,10 @@ class MIoTStorage:
|
||||
|
||||
User data will be stored in the `.storage` directory of Home Assistant.
|
||||
"""
|
||||
_main_loop: asyncio.AbstractEventLoop = None
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
_file_future: dict[str, tuple[MIoTStorageType, asyncio.Future]]
|
||||
|
||||
_root_path: str = None
|
||||
_root_path: str
|
||||
|
||||
def __init__(
|
||||
self, root_path: str,
|
||||
@ -140,7 +139,7 @@ class MIoTStorage:
|
||||
if r_data is None:
|
||||
_LOGGER.error('load error, empty file, %s', full_path)
|
||||
return None
|
||||
data_bytes: bytes = None
|
||||
data_bytes: bytes
|
||||
# Hash check
|
||||
if with_hash_check:
|
||||
if len(r_data) <= 32:
|
||||
@ -209,17 +208,17 @@ class MIoTStorage:
|
||||
else:
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
try:
|
||||
type_: type = type(data)
|
||||
w_bytes: bytes = None
|
||||
if type_ == bytes:
|
||||
w_bytes: bytes
|
||||
if isinstance(data, bytes):
|
||||
w_bytes = data
|
||||
elif type_ == str:
|
||||
elif isinstance(data, str):
|
||||
w_bytes = data.encode('utf-8')
|
||||
elif type_ in [dict, list]:
|
||||
elif isinstance(data, (dict, list)):
|
||||
w_bytes = json.dumps(data).encode('utf-8')
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'save error, unsupported data type, %s', type_.__name__)
|
||||
'save error, unsupported data type, %s',
|
||||
type(data).__name__)
|
||||
return False
|
||||
with open(full_path, 'wb') as w_file:
|
||||
w_file.write(w_bytes)
|
||||
@ -353,7 +352,8 @@ class MIoTStorage:
|
||||
def load_file(self, domain: str, name_with_suffix: str) -> Optional[bytes]:
|
||||
full_path = os.path.join(self._root_path, domain, name_with_suffix)
|
||||
return self.__load(
|
||||
full_path=full_path, type_=bytes, with_hash_check=False)
|
||||
full_path=full_path, type_=bytes,
|
||||
with_hash_check=False) # type: ignore
|
||||
|
||||
async def load_file_async(
|
||||
self, domain: str, name_with_suffix: str
|
||||
@ -371,7 +371,7 @@ class MIoTStorage:
|
||||
None, self.__load, full_path, bytes, False)
|
||||
if not fut.done():
|
||||
self.__add_file_future(full_path, MIoTStorageType.LOAD_FILE, fut)
|
||||
return await fut
|
||||
return await fut # type: ignore
|
||||
|
||||
def remove_file(self, domain: str, name_with_suffix: str) -> bool:
|
||||
full_path = os.path.join(self._root_path, domain, name_with_suffix)
|
||||
@ -438,7 +438,7 @@ class MIoTStorage:
|
||||
domain=config_domain, name=config_name, data=config)
|
||||
local_config = (self.load(domain=config_domain,
|
||||
name=config_name, type_=dict)) or {}
|
||||
local_config.update(config)
|
||||
local_config.update(config) # type: ignore
|
||||
return self.save(
|
||||
domain=config_domain, name=config_name, data=local_config)
|
||||
|
||||
@ -474,27 +474,31 @@ class MIoTStorage:
|
||||
domain=config_domain, name=config_name, data=config)
|
||||
local_config = (await self.load_async(
|
||||
domain=config_domain, name=config_name, type_=dict)) or {}
|
||||
local_config.update(config)
|
||||
local_config.update(config) # type: ignore
|
||||
return await self.save_async(
|
||||
domain=config_domain, name=config_name, data=local_config)
|
||||
|
||||
def load_user_config(
|
||||
self, uid: str, cloud_server: str, keys: Optional[list[str]] = None
|
||||
) -> dict[str, Any]:
|
||||
if keys is not None and len(keys) == 0:
|
||||
if isinstance(keys, list) and len(keys) == 0:
|
||||
# Do nothing
|
||||
return {}
|
||||
config_domain = 'miot_config'
|
||||
config_name = f'{uid}_{cloud_server}'
|
||||
local_config = (self.load(domain=config_domain,
|
||||
name=config_name, type_=dict)) or {}
|
||||
name=config_name, type_=dict))
|
||||
if not isinstance(local_config, dict):
|
||||
return {}
|
||||
if keys is None:
|
||||
return local_config
|
||||
return {key: local_config.get(key, None) for key in keys}
|
||||
return {
|
||||
key: local_config[key] for key in keys
|
||||
if key in local_config}
|
||||
|
||||
async def load_user_config_async(
|
||||
self, uid: str, cloud_server: str, keys: Optional[list[str]] = None
|
||||
) -> dict[str, Any]:
|
||||
) -> dict:
|
||||
"""Load user configuration.
|
||||
|
||||
Args:
|
||||
@ -505,13 +509,15 @@ class MIoTStorage:
|
||||
Returns:
|
||||
dict[str, Any]: query result
|
||||
"""
|
||||
if keys is not None and len(keys) == 0:
|
||||
if isinstance(keys, list) and len(keys) == 0:
|
||||
# Do nothing
|
||||
return {}
|
||||
config_domain = 'miot_config'
|
||||
config_name = f'{uid}_{cloud_server}'
|
||||
local_config = (await self.load_async(
|
||||
domain=config_domain, name=config_name, type_=dict)) or {}
|
||||
domain=config_domain, name=config_name, type_=dict))
|
||||
if not isinstance(local_config, dict):
|
||||
return {}
|
||||
if keys is None:
|
||||
return local_config
|
||||
return {
|
||||
@ -519,7 +525,8 @@ class MIoTStorage:
|
||||
if key in local_config}
|
||||
|
||||
def gen_storage_path(
|
||||
self, domain: str = None, name_with_suffix: str = None
|
||||
self, domain: Optional[str] = None,
|
||||
name_with_suffix: Optional[str] = None
|
||||
) -> str:
|
||||
"""Generate file path."""
|
||||
result = self._root_path
|
||||
@ -609,9 +616,8 @@ class MIoTCert:
|
||||
if cert_data is None:
|
||||
return 0
|
||||
# Check user cert
|
||||
user_cert: x509.Certificate = None
|
||||
try:
|
||||
user_cert = x509.load_pem_x509_certificate(
|
||||
user_cert: x509.Certificate = x509.load_pem_x509_certificate(
|
||||
cert_data, default_backend())
|
||||
cert_info = {}
|
||||
for attribute in user_cert.subject:
|
||||
@ -669,7 +675,8 @@ class MIoTCert:
|
||||
NameOID.COMMON_NAME, f'mips.{self._uid}.{did_hash}.2'),
|
||||
]))
|
||||
csr = builder.sign(
|
||||
private_key, algorithm=None, backend=default_backend())
|
||||
private_key, algorithm=None, # type: ignore
|
||||
backend=default_backend())
|
||||
return csr.public_bytes(serialization.Encoding.PEM).decode('utf-8')
|
||||
|
||||
async def load_user_key_async(self) -> Optional[str]:
|
||||
@ -719,250 +726,6 @@ class MIoTCert:
|
||||
return binascii.hexlify(sha1_hash.finalize()).decode('utf-8')
|
||||
|
||||
|
||||
class SpecMultiLang:
|
||||
"""
|
||||
MIoT-Spec-V2 multi-language for entities.
|
||||
"""
|
||||
MULTI_LANG_FILE = 'specs/multi_lang.json'
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
_lang: str
|
||||
_data: Optional[dict[str, dict]]
|
||||
|
||||
def __init__(
|
||||
self, lang: str, loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
) -> None:
|
||||
self._main_loop = loop or asyncio.get_event_loop()
|
||||
self._lang = lang
|
||||
self._data = None
|
||||
|
||||
async def init_async(self) -> None:
|
||||
if isinstance(self._data, dict):
|
||||
return
|
||||
multi_lang_data = None
|
||||
self._data = {}
|
||||
try:
|
||||
multi_lang_data = await self._main_loop.run_in_executor(
|
||||
None, load_json_file,
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
self.MULTI_LANG_FILE))
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.error('multi lang, load file error, %s', err)
|
||||
return
|
||||
# Check if the file is a valid JSON file
|
||||
if not isinstance(multi_lang_data, dict):
|
||||
_LOGGER.error('multi lang, invalid file data')
|
||||
return
|
||||
for lang_data in multi_lang_data.values():
|
||||
if not isinstance(lang_data, dict):
|
||||
_LOGGER.error('multi lang, invalid lang data')
|
||||
return
|
||||
for data in lang_data.values():
|
||||
if not isinstance(data, dict):
|
||||
_LOGGER.error('multi lang, invalid lang data item')
|
||||
return
|
||||
self._data = multi_lang_data
|
||||
|
||||
async def deinit_async(self) -> str:
|
||||
self._data = None
|
||||
|
||||
async def translate_async(self, urn_key: str) -> dict[str, str]:
|
||||
"""MUST call init_async() first."""
|
||||
if urn_key in self._data:
|
||||
return self._data[urn_key].get(self._lang, {})
|
||||
return {}
|
||||
|
||||
|
||||
class SpecBoolTranslation:
|
||||
"""
|
||||
Boolean value translation.
|
||||
"""
|
||||
BOOL_TRANS_FILE = 'specs/bool_trans.json'
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
_lang: str
|
||||
_data: Optional[dict[str, dict]]
|
||||
_data_default: dict[str, dict]
|
||||
|
||||
def __init__(
|
||||
self, lang: str, loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
) -> None:
|
||||
self._main_loop = loop or asyncio.get_event_loop()
|
||||
self._lang = lang
|
||||
self._data = None
|
||||
|
||||
async def init_async(self) -> None:
|
||||
if isinstance(self._data, dict):
|
||||
return
|
||||
data = None
|
||||
self._data = {}
|
||||
try:
|
||||
data = await self._main_loop.run_in_executor(
|
||||
None, load_json_file,
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
self.BOOL_TRANS_FILE))
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.error('bool trans, load file error, %s', err)
|
||||
return
|
||||
# Check if the file is a valid JSON file
|
||||
if (
|
||||
not isinstance(data, dict)
|
||||
or 'data' not in data
|
||||
or not isinstance(data['data'], dict)
|
||||
or 'translate' not in data
|
||||
or not isinstance(data['translate'], dict)
|
||||
):
|
||||
_LOGGER.error('bool trans, valid file')
|
||||
return
|
||||
|
||||
if 'default' in data['translate']:
|
||||
data_default = (
|
||||
data['translate']['default'].get(self._lang, None)
|
||||
or data['translate']['default'].get(
|
||||
DEFAULT_INTEGRATION_LANGUAGE, None))
|
||||
if data_default:
|
||||
self._data_default = [
|
||||
{'value': True, 'description': data_default['true']},
|
||||
{'value': False, 'description': data_default['false']}
|
||||
]
|
||||
|
||||
for urn, key in data['data'].items():
|
||||
if key not in data['translate']:
|
||||
_LOGGER.error('bool trans, unknown key, %s, %s', urn, key)
|
||||
continue
|
||||
trans_data = (
|
||||
data['translate'][key].get(self._lang, None)
|
||||
or data['translate'][key].get(
|
||||
DEFAULT_INTEGRATION_LANGUAGE, None))
|
||||
if trans_data:
|
||||
self._data[urn] = [
|
||||
{'value': True, 'description': trans_data['true']},
|
||||
{'value': False, 'description': trans_data['false']}
|
||||
]
|
||||
|
||||
async def deinit_async(self) -> None:
|
||||
self._data = None
|
||||
self._data_default = None
|
||||
|
||||
async def translate_async(self, urn: str) -> list[dict[bool, str]]:
|
||||
"""
|
||||
MUST call init_async() before calling this method.
|
||||
[
|
||||
{'value': True, 'description': 'True'},
|
||||
{'value': False, 'description': 'False'}
|
||||
]
|
||||
"""
|
||||
|
||||
return self._data.get(urn, self._data_default)
|
||||
|
||||
|
||||
class SpecFilter:
|
||||
"""
|
||||
MIoT-Spec-V2 filter for entity conversion.
|
||||
"""
|
||||
SPEC_FILTER_FILE = 'specs/spec_filter.json'
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
_data: dict[str, dict[str, set]]
|
||||
_cache: Optional[dict]
|
||||
|
||||
def __init__(self, loop: Optional[asyncio.AbstractEventLoop]) -> None:
|
||||
self._main_loop = loop or asyncio.get_event_loop()
|
||||
self._data = None
|
||||
self._cache = None
|
||||
|
||||
async def init_async(self) -> None:
|
||||
if isinstance(self._data, dict):
|
||||
return
|
||||
filter_data = None
|
||||
self._data = {}
|
||||
try:
|
||||
filter_data = await self._main_loop.run_in_executor(
|
||||
None, load_json_file,
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
self.SPEC_FILTER_FILE))
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.error('spec filter, load file error, %s', err)
|
||||
return
|
||||
if not isinstance(filter_data, dict):
|
||||
_LOGGER.error('spec filter, invalid spec filter content')
|
||||
return
|
||||
for values in list(filter_data.values()):
|
||||
if not isinstance(values, dict):
|
||||
_LOGGER.error('spec filter, invalid spec filter data')
|
||||
return
|
||||
for value in values.values():
|
||||
if not isinstance(value, list):
|
||||
_LOGGER.error('spec filter, invalid spec filter rules')
|
||||
return
|
||||
|
||||
self._data = filter_data
|
||||
|
||||
async def deinit_async(self) -> None:
|
||||
self._cache = None
|
||||
self._data = None
|
||||
|
||||
def filter_spec(self, urn_key: str) -> None:
|
||||
"""MUST call init_async() first."""
|
||||
if not self._data:
|
||||
return
|
||||
self._cache = self._data.get(urn_key, None)
|
||||
|
||||
def filter_service(self, siid: int) -> bool:
|
||||
"""Filter service by siid.
|
||||
MUST call init_async() and filter_spec() first."""
|
||||
if (
|
||||
self._cache
|
||||
and 'services' in self._cache
|
||||
and (
|
||||
str(siid) in self._cache['services']
|
||||
or '*' in self._cache['services'])
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def filter_property(self, siid: int, piid: int) -> bool:
|
||||
"""Filter property by piid.
|
||||
MUST call init_async() and filter_spec() first."""
|
||||
if (
|
||||
self._cache
|
||||
and 'properties' in self._cache
|
||||
and (
|
||||
f'{siid}.{piid}' in self._cache['properties']
|
||||
or f'{siid}.*' in self._cache['properties'])
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def filter_event(self, siid: int, eiid: int) -> bool:
|
||||
"""Filter event by eiid.
|
||||
MUST call init_async() and filter_spec() first."""
|
||||
if (
|
||||
self._cache
|
||||
and 'events' in self._cache
|
||||
and (
|
||||
f'{siid}.{eiid}' in self._cache['events']
|
||||
or f'{siid}.*' in self._cache['events']
|
||||
)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def filter_action(self, siid: int, aiid: int) -> bool:
|
||||
""""Filter action by aiid.
|
||||
MUST call init_async() and filter_spec() first."""
|
||||
if (
|
||||
self._cache
|
||||
and 'actions' in self._cache
|
||||
and (
|
||||
f'{siid}.{aiid}' in self._cache['actions']
|
||||
or f'{siid}.*' in self._cache['actions'])
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class DeviceManufacturer:
|
||||
"""Device manufacturer."""
|
||||
DOMAIN: str = 'miot_specs'
|
||||
@ -976,12 +739,11 @@ class DeviceManufacturer:
|
||||
) -> None:
|
||||
self._main_loop = loop or asyncio.get_event_loop()
|
||||
self._storage = storage
|
||||
self._data = None
|
||||
self._data = {}
|
||||
|
||||
async def init_async(self) -> None:
|
||||
if self._data:
|
||||
return
|
||||
data_cache: dict = None
|
||||
data_cache = await self._storage.load_async(
|
||||
domain=self.DOMAIN, name='manufacturer', type_=dict)
|
||||
if (
|
||||
@ -995,8 +757,15 @@ class DeviceManufacturer:
|
||||
_LOGGER.debug('load manufacturer data success')
|
||||
return
|
||||
|
||||
data_cloud = await self._main_loop.run_in_executor(
|
||||
None, self.__get_manufacturer_data)
|
||||
data_cloud = None
|
||||
try:
|
||||
data_cloud = await MIoTHttp.get_json_async(
|
||||
url='https://cdn.cnbj1.fds.api.mi-img.com/res-conf/xiaomi-home/'
|
||||
'manufacturer.json',
|
||||
loop=self._main_loop)
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.error('get manufacturer info failed, %s', err)
|
||||
|
||||
if data_cloud:
|
||||
await self._storage.save_async(
|
||||
domain=self.DOMAIN, name='manufacturer',
|
||||
@ -1004,32 +773,16 @@ class DeviceManufacturer:
|
||||
self._data = data_cloud
|
||||
_LOGGER.debug('update manufacturer data success')
|
||||
else:
|
||||
if data_cache:
|
||||
self._data = data_cache.get('data', None)
|
||||
if isinstance(data_cache, dict):
|
||||
self._data = data_cache.get('data', {})
|
||||
_LOGGER.error('load manufacturer data failed, use local data')
|
||||
else:
|
||||
_LOGGER.error('load manufacturer data failed')
|
||||
|
||||
async def deinit_async(self) -> None:
|
||||
self._data = None
|
||||
self._data.clear()
|
||||
|
||||
def get_name(self, short_name: str) -> str:
|
||||
if not self._data or not short_name or short_name not in self._data:
|
||||
return short_name
|
||||
return self._data[short_name].get('name', None) or short_name
|
||||
|
||||
def __get_manufacturer_data(self) -> dict:
|
||||
try:
|
||||
request = Request(
|
||||
'https://cdn.cnbj1.fds.api.mi-img.com/res-conf/xiaomi-home/'
|
||||
'manufacturer.json',
|
||||
method='GET')
|
||||
content: bytes = None
|
||||
with urlopen(request) as response:
|
||||
content = response.read()
|
||||
return (
|
||||
json.loads(str(content, 'utf-8'))
|
||||
if content else None)
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.error('get manufacturer info failed, %s', err)
|
||||
return None
|
||||
|
@ -1,315 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"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: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",
|
||||
"urn:miot-spec-v2:property:guard-mode:000000B6": "open_close",
|
||||
"urn:miot-spec-v2:property:heater:00000026": "open_close",
|
||||
"urn:miot-spec-v2:property:heating:000000C7": "open_close",
|
||||
"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: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",
|
||||
"urn:miot-spec-v2:property:valve-switch:000000FE": "open_close",
|
||||
"urn:miot-spec-v2:property:ventilation:000000CE": "open_close",
|
||||
"urn:miot-spec-v2:property:vertical-swing:00000018": "open_close",
|
||||
"urn:miot-spec-v2:property:wake-up-mode:00000107": "open_close",
|
||||
"urn:miot-spec-v2:property:water-pump:000000F2": "open_close",
|
||||
"urn:miot-spec-v2:property:watering:000000CC": "open_close",
|
||||
"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:wifi-ssid-hidden:000000E3": "yes_no",
|
||||
"urn:miot-spec-v2:property:wind-reverse:00000117": "yes_no"
|
||||
},
|
||||
"translate": {
|
||||
"default": {
|
||||
"de": {
|
||||
"true": "Wahr",
|
||||
"false": "Falsch"
|
||||
},
|
||||
"en": {
|
||||
"true": "True",
|
||||
"false": "False"
|
||||
},
|
||||
"es": {
|
||||
"true": "Verdadero",
|
||||
"false": "Falso"
|
||||
},
|
||||
"fr": {
|
||||
"true": "Vrai",
|
||||
"false": "Faux"
|
||||
},
|
||||
"it": {
|
||||
"true": "Vero",
|
||||
"false": "Falso"
|
||||
},
|
||||
"ja": {
|
||||
"true": "真",
|
||||
"false": "偽"
|
||||
},
|
||||
"nl": {
|
||||
"true": "True",
|
||||
"false": "False"
|
||||
},
|
||||
"pt": {
|
||||
"true": "True",
|
||||
"false": "False"
|
||||
},
|
||||
"pt-BR": {
|
||||
"true": "True",
|
||||
"false": "False"
|
||||
},
|
||||
"ru": {
|
||||
"true": "Истина",
|
||||
"false": "Ложь"
|
||||
},
|
||||
"zh-Hans": {
|
||||
"true": "真",
|
||||
"false": "假"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"true": "真",
|
||||
"false": "假"
|
||||
}
|
||||
},
|
||||
"open_close": {
|
||||
"de": {
|
||||
"true": "Öffnen",
|
||||
"false": "Schließen"
|
||||
},
|
||||
"en": {
|
||||
"true": "Open",
|
||||
"false": "Close"
|
||||
},
|
||||
"es": {
|
||||
"true": "Abierto",
|
||||
"false": "Cerrado"
|
||||
},
|
||||
"fr": {
|
||||
"true": "Ouvert",
|
||||
"false": "Fermer"
|
||||
},
|
||||
"it": {
|
||||
"true": "Aperto",
|
||||
"false": "Chiuso"
|
||||
},
|
||||
"ja": {
|
||||
"true": "開く",
|
||||
"false": "閉じる"
|
||||
},
|
||||
"nl": {
|
||||
"true": "Open",
|
||||
"false": "Dicht"
|
||||
},
|
||||
"pt": {
|
||||
"true": "Aberto",
|
||||
"false": "Fechado"
|
||||
},
|
||||
"pt-BR": {
|
||||
"true": "Aberto",
|
||||
"false": "Fechado"
|
||||
},
|
||||
"ru": {
|
||||
"true": "Открыть",
|
||||
"false": "Закрыть"
|
||||
},
|
||||
"zh-Hans": {
|
||||
"true": "开启",
|
||||
"false": "关闭"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"true": "開啟",
|
||||
"false": "關閉"
|
||||
}
|
||||
},
|
||||
"yes_no": {
|
||||
"de": {
|
||||
"true": "Ja",
|
||||
"false": "Nein"
|
||||
},
|
||||
"en": {
|
||||
"true": "Yes",
|
||||
"false": "No"
|
||||
},
|
||||
"es": {
|
||||
"true": "Sí",
|
||||
"false": "No"
|
||||
},
|
||||
"fr": {
|
||||
"true": "Oui",
|
||||
"false": "Non"
|
||||
},
|
||||
"it": {
|
||||
"true": "Si",
|
||||
"false": "No"
|
||||
},
|
||||
"ja": {
|
||||
"true": "はい",
|
||||
"false": "いいえ"
|
||||
},
|
||||
"nl": {
|
||||
"true": "Ja",
|
||||
"false": "Nee"
|
||||
},
|
||||
"pt": {
|
||||
"true": "Sim",
|
||||
"false": "Não"
|
||||
},
|
||||
"pt-BR": {
|
||||
"true": "Sim",
|
||||
"false": "Não"
|
||||
},
|
||||
"ru": {
|
||||
"true": "Да",
|
||||
"false": "Нет"
|
||||
},
|
||||
"zh-Hans": {
|
||||
"true": "是",
|
||||
"false": "否"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"true": "是",
|
||||
"false": "否"
|
||||
}
|
||||
},
|
||||
"motion_state": {
|
||||
"de": {
|
||||
"true": "Bewegung erkannt",
|
||||
"false": "Keine Bewegung erkannt"
|
||||
},
|
||||
"en": {
|
||||
"true": "Motion Detected",
|
||||
"false": "No Motion Detected"
|
||||
},
|
||||
"es": {
|
||||
"true": "Movimiento detectado",
|
||||
"false": "No se detecta movimiento"
|
||||
},
|
||||
"fr": {
|
||||
"true": "Mouvement détecté",
|
||||
"false": "Aucun mouvement détecté"
|
||||
},
|
||||
"it": {
|
||||
"true": "Movimento Rilevato",
|
||||
"false": "Nessun Movimento Rilevato"
|
||||
},
|
||||
"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": "Движение не обнаружено"
|
||||
},
|
||||
"zh-Hans": {
|
||||
"true": "有人",
|
||||
"false": "无人"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"true": "有人",
|
||||
"false": "無人"
|
||||
}
|
||||
},
|
||||
"contact_state": {
|
||||
"de": {
|
||||
"true": "Kontakt",
|
||||
"false": "Kein Kontakt"
|
||||
},
|
||||
"en": {
|
||||
"true": "Contact",
|
||||
"false": "No Contact"
|
||||
},
|
||||
"es": {
|
||||
"true": "Contacto",
|
||||
"false": "Sin contacto"
|
||||
},
|
||||
"fr": {
|
||||
"true": "Contact",
|
||||
"false": "Pas de contact"
|
||||
},
|
||||
"it": {
|
||||
"true": "Contatto",
|
||||
"false": "Nessun Contatto"
|
||||
},
|
||||
"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": "Нет контакта"
|
||||
},
|
||||
"zh-Hans": {
|
||||
"true": "接触",
|
||||
"false": "分离"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"true": "接觸",
|
||||
"false": "分離"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
246
custom_components/xiaomi_home/miot/specs/bool_trans.yaml
Normal file
246
custom_components/xiaomi_home/miot/specs/bool_trans.yaml
Normal file
@ -0,0 +1,246 @@
|
||||
data:
|
||||
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: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
|
||||
urn:miot-spec-v2:property:guard-mode:000000B6: open_close
|
||||
urn:miot-spec-v2:property:heater:00000026: open_close
|
||||
urn:miot-spec-v2:property:heating:000000C7: open_close
|
||||
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: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
|
||||
urn:miot-spec-v2:property:valve-switch:000000FE: open_close
|
||||
urn:miot-spec-v2:property:ventilation:000000CE: open_close
|
||||
urn:miot-spec-v2:property:vertical-swing:00000018: open_close
|
||||
urn:miot-spec-v2:property:wake-up-mode:00000107: open_close
|
||||
urn:miot-spec-v2:property:water-pump:000000F2: open_close
|
||||
urn:miot-spec-v2:property:watering:000000CC: open_close
|
||||
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:wifi-ssid-hidden:000000E3: yes_no
|
||||
urn:miot-spec-v2:property:wind-reverse:00000117: yes_no
|
||||
translate:
|
||||
contact_state:
|
||||
de:
|
||||
'false': Kein Kontakt
|
||||
'true': Kontakt
|
||||
en:
|
||||
'false': No Contact
|
||||
'true': Contact
|
||||
es:
|
||||
'false': Sin contacto
|
||||
'true': Contacto
|
||||
fr:
|
||||
'false': Pas de contact
|
||||
'true': Contact
|
||||
it:
|
||||
'false': Nessun contatto
|
||||
'true': Contatto
|
||||
ja:
|
||||
'false': 非接触
|
||||
'true': 接触
|
||||
nl:
|
||||
'false': Geen contact
|
||||
'true': Contact
|
||||
pt:
|
||||
'false': Sem contato
|
||||
'true': Contato
|
||||
pt-BR:
|
||||
'false': Sem contato
|
||||
'true': Contato
|
||||
ru:
|
||||
'false': Нет контакта
|
||||
'true': Контакт
|
||||
zh-Hans:
|
||||
'false': 分离
|
||||
'true': 接触
|
||||
zh-Hant:
|
||||
'false': 分離
|
||||
'true': 接觸
|
||||
default:
|
||||
de:
|
||||
'false': Falsch
|
||||
'true': Wahr
|
||||
en:
|
||||
'false': 'False'
|
||||
'true': 'True'
|
||||
es:
|
||||
'false': Falso
|
||||
'true': Verdadero
|
||||
fr:
|
||||
'false': Faux
|
||||
'true': Vrai
|
||||
it:
|
||||
'false': Falso
|
||||
'true': Vero
|
||||
ja:
|
||||
'false': 偽
|
||||
'true': 真
|
||||
nl:
|
||||
'false': 'False'
|
||||
'true': 'True'
|
||||
pt:
|
||||
'false': 'False'
|
||||
'true': 'True'
|
||||
pt-BR:
|
||||
'false': 'False'
|
||||
'true': 'True'
|
||||
ru:
|
||||
'false': Ложь
|
||||
'true': Истина
|
||||
zh-Hans:
|
||||
'false': 假
|
||||
'true': 真
|
||||
zh-Hant:
|
||||
'false': 假
|
||||
'true': 真
|
||||
motion_state:
|
||||
de:
|
||||
'false': Keine Bewegung erkannt
|
||||
'true': Bewegung erkannt
|
||||
en:
|
||||
'false': No Motion Detected
|
||||
'true': Motion Detected
|
||||
es:
|
||||
'false': No se detecta movimiento
|
||||
'true': Movimiento detectado
|
||||
fr:
|
||||
'false': Aucun mouvement détecté
|
||||
'true': Mouvement détecté
|
||||
it:
|
||||
'false': Nessun Movimento Rilevato
|
||||
'true': Movimento Rilevato
|
||||
ja:
|
||||
'false': 動きが検出されません
|
||||
'true': 動きを検知
|
||||
nl:
|
||||
'false': Geen contact
|
||||
'true': Contact
|
||||
pt:
|
||||
'false': Sem contato
|
||||
'true': Contato
|
||||
pt-BR:
|
||||
'false': Sem contato
|
||||
'true': Contato
|
||||
ru:
|
||||
'false': Движение не обнаружено
|
||||
'true': Обнаружено движение
|
||||
zh-Hans:
|
||||
'false': 无人
|
||||
'true': 有人
|
||||
zh-Hant:
|
||||
'false': 無人
|
||||
'true': 有人
|
||||
open_close:
|
||||
de:
|
||||
'false': Schließen
|
||||
'true': Öffnen
|
||||
en:
|
||||
'false': Close
|
||||
'true': Open
|
||||
es:
|
||||
'false': Cerrado
|
||||
'true': Abierto
|
||||
fr:
|
||||
'false': Fermer
|
||||
'true': Ouvert
|
||||
it:
|
||||
'false': Chiuso
|
||||
'true': Aperto
|
||||
ja:
|
||||
'false': 閉じる
|
||||
'true': 開く
|
||||
nl:
|
||||
'false': Dicht
|
||||
'true': Open
|
||||
pt:
|
||||
'false': Fechado
|
||||
'true': Aberto
|
||||
pt-BR:
|
||||
'false': Fechado
|
||||
'true': Aberto
|
||||
ru:
|
||||
'false': Закрыть
|
||||
'true': Открыть
|
||||
zh-Hans:
|
||||
'false': 关闭
|
||||
'true': 开启
|
||||
zh-Hant:
|
||||
'false': 關閉
|
||||
'true': 開啟
|
||||
yes_no:
|
||||
de:
|
||||
'false': Nein
|
||||
'true': Ja
|
||||
en:
|
||||
'false': 'No'
|
||||
'true': 'Yes'
|
||||
es:
|
||||
'false': 'No'
|
||||
'true': Sí
|
||||
fr:
|
||||
'false': Non
|
||||
'true': Oui
|
||||
it:
|
||||
'false': 'No'
|
||||
'true': Si
|
||||
ja:
|
||||
'false': いいえ
|
||||
'true': はい
|
||||
nl:
|
||||
'false': Nee
|
||||
'true': Ja
|
||||
pt:
|
||||
'false': Não
|
||||
'true': Sim
|
||||
pt-BR:
|
||||
'false': Não
|
||||
'true': Sim
|
||||
ru:
|
||||
'false': Нет
|
||||
'true': Да
|
||||
zh-Hans:
|
||||
'false': 否
|
||||
'true': 是
|
||||
zh-Hant:
|
||||
'false': 否
|
||||
'true': 是
|
@ -1,194 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
"service:001:property:005": "Serial Number (SN)",
|
||||
"service:002": "Gateway",
|
||||
"service:002:event:001": "Network Changed",
|
||||
"service:002:event:002": "Network Changed",
|
||||
"service:002:property:001": "Access Method",
|
||||
"service:002:property:001:valuelist:000": "Wired",
|
||||
"service:002:property:001:valuelist:001": "5G Wireless",
|
||||
"service:002:property:001:valuelist:002": "2.4G Wireless",
|
||||
"service:002:property:002": "IP Address",
|
||||
"service:002:property:003": "WiFi Network Name",
|
||||
"service:002:property:004": "Current Time",
|
||||
"service:002:property:005": "DHCP Server MAC Address",
|
||||
"service:003": "Indicator Light",
|
||||
"service:003:property:001": "Switch",
|
||||
"service:004": "Virtual Service",
|
||||
"service:004:action:001": "Generate Virtual Event",
|
||||
"service:004:event:001": "Virtual Event Occurred",
|
||||
"service:004:property:001": "Event Name"
|
||||
},
|
||||
"es": {
|
||||
"service:001": "Información del dispositivo",
|
||||
"service:001:property:003": "ID del dispositivo",
|
||||
"service:001:property:005": "Número de serie (SN)",
|
||||
"service:002": "Puerta de enlace",
|
||||
"service:002:event:001": "Cambio de red",
|
||||
"service:002:event:002": "Cambio de red",
|
||||
"service:002:property:001": "Método de acceso",
|
||||
"service:002:property:001:valuelist:000": "Cableado",
|
||||
"service:002:property:001:valuelist:001": "5G inalámbrico",
|
||||
"service:002:property:001:valuelist:002": "2.4G inalámbrico",
|
||||
"service:002:property:002": "Dirección IP",
|
||||
"service:002:property:003": "Nombre de red WiFi",
|
||||
"service:002:property:004": "Hora actual",
|
||||
"service:002:property:005": "Dirección MAC del servidor DHCP",
|
||||
"service:003": "Luz indicadora",
|
||||
"service:003:property:001": "Interruptor",
|
||||
"service:004": "Servicio virtual",
|
||||
"service:004:action:001": "Generar evento virtual",
|
||||
"service:004:event:001": "Ocurrió un evento virtual",
|
||||
"service:004:property:001": "Nombre del evento"
|
||||
},
|
||||
"fr": {
|
||||
"service:001": "Informations sur l'appareil",
|
||||
"service:001:property:003": "ID de l'appareil",
|
||||
"service:001:property:005": "Numéro de série (SN)",
|
||||
"service:002": "Passerelle",
|
||||
"service:002:event:001": "Changement de réseau",
|
||||
"service:002:event:002": "Changement de réseau",
|
||||
"service:002:property:001": "Méthode d'accès",
|
||||
"service:002:property:001:valuelist:000": "Câblé",
|
||||
"service:002:property:001:valuelist:001": "Sans fil 5G",
|
||||
"service:002:property:001:valuelist:002": "Sans fil 2.4G",
|
||||
"service:002:property:002": "Adresse IP",
|
||||
"service:002:property:003": "Nom du réseau WiFi",
|
||||
"service:002:property:004": "Heure actuelle",
|
||||
"service:002:property:005": "Adresse MAC du serveur DHCP",
|
||||
"service:003": "Voyant lumineux",
|
||||
"service:003:property:001": "Interrupteur",
|
||||
"service:004": "Service virtuel",
|
||||
"service:004:action:001": "Générer un événement virtuel",
|
||||
"service:004:event:001": "Événement virtuel survenu",
|
||||
"service:004:property:001": "Nom de l'événement"
|
||||
},
|
||||
"it": {
|
||||
"service:001": "Informazioni sul Dispositivo",
|
||||
"service:001:property:003": "ID Dispositivo",
|
||||
"service:001:property:005": "Numero di Serie (SN)",
|
||||
"service:002": "Gateway",
|
||||
"service:002:event:001": "Rete Modificata",
|
||||
"service:002:event:002": "Rete Modificata",
|
||||
"service:002:property:001": "Metodo di Accesso",
|
||||
"service:002:property:001:valuelist:000": "Cablato",
|
||||
"service:002:property:001:valuelist:001": "Wireless 5G",
|
||||
"service:002:property:001:valuelist:002": "Wireless 2.4G",
|
||||
"service:002:property:002": "Indirizzo IP",
|
||||
"service:002:property:003": "Nome Rete WiFi",
|
||||
"service:002:property:004": "Ora Attuale",
|
||||
"service:002:property:005": "Indirizzo MAC del Server DHCP",
|
||||
"service:003": "Luce Indicatore",
|
||||
"service:003:property:001": "Interruttore",
|
||||
"service:004": "Servizio Virtuale",
|
||||
"service:004:action:001": "Genera Evento Virtuale",
|
||||
"service:004:event:001": "Evento Virtuale Avvenuto",
|
||||
"service:004:property:001": "Nome Evento"
|
||||
},
|
||||
"ja": {
|
||||
"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": "DHCPサーバーMACアドレス",
|
||||
"service:003": "インジケータライト",
|
||||
"service:003:property:001": "スイッチ",
|
||||
"service:004": "バーチャルサービス",
|
||||
"service:004:action:001": "バーチャルイベントを生成",
|
||||
"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",
|
||||
"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": "DHCP伺服器MAC地址",
|
||||
"service:003": "指示燈",
|
||||
"service:003:property:001": "開關",
|
||||
"service:004": "虛擬服務",
|
||||
"service:004:action:001": "產生虛擬事件",
|
||||
"service:004:event:001": "虛擬事件發生",
|
||||
"service:004:property:001": "事件名稱"
|
||||
}
|
||||
},
|
||||
"urn:miot-spec-v2:device:switch:0000A003:lumi-acn040:1": {
|
||||
"en": {
|
||||
"service:011": "Right Button On and Off",
|
||||
"service:011:property:001": "Right Button On and Off",
|
||||
"service:015:action:001": "Left Button Identify",
|
||||
"service:016:action:001": "Middle Button Identify",
|
||||
"service:017:action:001": "Right Button Identify"
|
||||
},
|
||||
"zh-Hans": {
|
||||
"service:015:action:001": "左键确认",
|
||||
"service:016:action:001": "中键确认",
|
||||
"service:017:action:001": "右键确认"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
{
|
||||
"urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4": {
|
||||
"properties": [
|
||||
"9.*",
|
||||
"13.*",
|
||||
"15.*"
|
||||
],
|
||||
"services": [
|
||||
"10"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01": {
|
||||
"properties": [
|
||||
"5.1"
|
||||
],
|
||||
"services": [
|
||||
"4",
|
||||
"7",
|
||||
"8"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": {
|
||||
"events": [
|
||||
"2.1"
|
||||
]
|
||||
},
|
||||
"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",
|
||||
"3"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:light:0000A001:yeelink-color2": {
|
||||
"properties": [
|
||||
"3.*",
|
||||
"2.5"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:light:0000A001:yeelink-dnlight2": {
|
||||
"services": [
|
||||
"3"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:light:0000A001:yeelink-mbulb3": {
|
||||
"services": [
|
||||
"3"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1": {
|
||||
"services": [
|
||||
"1",
|
||||
"5"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03": {
|
||||
"services": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
}
|
43
custom_components/xiaomi_home/miot/specs/spec_filter.yaml
Normal file
43
custom_components/xiaomi_home/miot/specs/spec_filter.yaml
Normal file
@ -0,0 +1,43 @@
|
||||
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4:
|
||||
properties:
|
||||
- 9.*
|
||||
- 13.*
|
||||
- 15.*
|
||||
services:
|
||||
- '10'
|
||||
urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01:
|
||||
properties:
|
||||
- '5.1'
|
||||
services:
|
||||
- '4'
|
||||
- '7'
|
||||
- '8'
|
||||
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:
|
||||
events:
|
||||
- '2.1'
|
||||
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'
|
||||
- '3'
|
||||
urn:miot-spec-v2:device:light:0000A001:yeelink-color2:
|
||||
properties:
|
||||
- 3.*
|
||||
- '2.5'
|
||||
urn:miot-spec-v2:device:light:0000A001:yeelink-dnlight2:
|
||||
services:
|
||||
- '3'
|
||||
urn:miot-spec-v2:device:light:0000A001:yeelink-mbulb3:
|
||||
services:
|
||||
- '3'
|
||||
urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1:
|
||||
services:
|
||||
- '1'
|
||||
- '5'
|
||||
urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03:
|
||||
services:
|
||||
- '*'
|
Reference in New Issue
Block a user