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:
Paul Shawn
2025-01-17 18:14:31 +08:00
committed by GitHub
parent bf116e13a4
commit ef56448dbb
54 changed files with 2050 additions and 1978 deletions

View File

@ -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)

View File

@ -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",

View File

@ -54,6 +54,10 @@
"enable": "Enable",
"disable": "Disable"
},
"binary_mode": {
"text": "Text Sensor Entity",
"bool": "Binary Sensor Entity"
},
"device_state": {
"add": "Add",
"del": "Unavailable",

View File

@ -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",

View File

@ -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",

View File

@ -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"
}
}
}
}

View File

@ -54,6 +54,10 @@
"enable": "有効",
"disable": "無効"
},
"binary_mode": {
"text": "テキストセンサーエンティティ",
"bool": "バイナリセンサーエンティティ"
},
"device_state": {
"add": "追加",
"del": "利用不可",

View File

@ -54,6 +54,10 @@
"enable": "Inschakelen",
"disable": "Uitschakelen"
},
"binary_mode": {
"text": "Tekstsensor-entiteit",
"bool": "Binairesensor-entiteit"
},
"device_state": {
"add": "Toevoegen",
"del": "Niet beschikbaar",

View File

@ -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",

View File

@ -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",

View File

@ -54,6 +54,10 @@
"enable": "Включить",
"disable": "Отключить"
},
"binary_mode": {
"text": "Сущность текстового датчика",
"bool": "Сущность бинарного датчика"
},
"device_state": {
"add": "Добавить",
"del": "Недоступно",

View File

@ -54,6 +54,10 @@
"enable": "启用",
"disable": "禁用"
},
"binary_mode": {
"text": "文本传感器实体",
"bool": "二进制传感器实体"
},
"device_state": {
"add": "新增",
"del": "不可用",

View File

@ -54,6 +54,10 @@
"enable": "啟用",
"disable": "禁用"
},
"binary_mode": {
"text": "文本傳感器實體",
"bool": "二進制傳感器實體"
},
"device_state": {
"add": "新增",
"del": "不可用",

View File

@ -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)

View File

@ -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(

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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": "分離"
}
}
}
}

View 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':
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':

View File

@ -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": "右键确认"
}
}
}

View File

@ -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": [
"*"
]
}
}

View 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:
- '*'