mirror of
https://github.com/XiaoMi/ha_xiaomi_home.git
synced 2025-03-28 21:35:29 +08:00
Fix local ctrl error (#271)
* feat: common.py add gen_absolute_path, load_yaml_file * fix: miot_lan add profile devices filter * fix: add lan ctrl profile model list * test: improve lan test * fix: fix pylint redefined-outer-name * feat: update tools to update profile models file * fix: fix pylint waning * fix: update miot lan NETWORK_UNSTABLE_RESUME_TH value
This commit is contained in:
parent
6ce3206b30
commit
bd3a98b976
@ -46,10 +46,19 @@ off Xiaomi or its affiliates' products.
|
||||
Common utilities.
|
||||
"""
|
||||
import json
|
||||
from os import path
|
||||
import random
|
||||
from typing import Optional
|
||||
import hashlib
|
||||
from paho.mqtt.client import MQTTMatcher
|
||||
import yaml
|
||||
|
||||
MIOT_ROOT_PATH: str = path.dirname(path.abspath(__file__))
|
||||
|
||||
|
||||
def gen_absolute_path(relative_path: str) -> str:
|
||||
"""Generate an absolute path."""
|
||||
return path.join(MIOT_ROOT_PATH, relative_path)
|
||||
|
||||
|
||||
def calc_group_id(uid: str, home_id: str) -> str:
|
||||
@ -64,6 +73,12 @@ def load_json_file(json_file: str) -> dict:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_yaml_file(yaml_file: str) -> dict:
|
||||
"""Load a YAML file."""
|
||||
with open(yaml_file, 'r', encoding='utf-8') as f:
|
||||
return yaml.load(f, Loader=yaml.FullLoader)
|
||||
|
||||
|
||||
def randomize_int(value: int, ratio: float) -> int:
|
||||
"""Randomize an integer value."""
|
||||
return int(value * (1 - ratio + random.random()*2*ratio))
|
||||
@ -74,12 +89,12 @@ class MIoTMatcher(MQTTMatcher):
|
||||
|
||||
def iter_all_nodes(self) -> any:
|
||||
"""Return an iterator on all nodes with their paths and contents."""
|
||||
def rec(node, path):
|
||||
def rec(node, path_):
|
||||
# pylint: disable=protected-access
|
||||
if node._content:
|
||||
yield ('/'.join(path), node._content)
|
||||
yield ('/'.join(path_), node._content)
|
||||
for part, child in node._children.items():
|
||||
yield from rec(child, path + [part])
|
||||
yield from rec(child, path_ + [part])
|
||||
return rec(self._root, [])
|
||||
|
||||
def get(self, topic: str) -> Optional[any]:
|
||||
|
1320
custom_components/xiaomi_home/miot/lan/profile_models.yaml
Normal file
1320
custom_components/xiaomi_home/miot/lan/profile_models.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@ -71,7 +71,8 @@ from .miot_error import MIoTErrorCode
|
||||
from .miot_ev import MIoTEventLoop, TimeoutHandle
|
||||
from .miot_network import InterfaceStatus, MIoTNetwork, NetworkInfo
|
||||
from .miot_mdns import MipsService, MipsServiceState
|
||||
from .common import randomize_int, MIoTMatcher
|
||||
from .common import (
|
||||
randomize_int, load_yaml_file, gen_absolute_path, MIoTMatcher)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -175,7 +176,7 @@ class MIoTLanDevice:
|
||||
OT_HEADER_LEN: int = 32
|
||||
NETWORK_UNSTABLE_CNT_TH: int = 10
|
||||
NETWORK_UNSTABLE_TIME_TH: int = 120000
|
||||
NETWORK_UNSTABLE_RESUME_TH: int = 300
|
||||
NETWORK_UNSTABLE_RESUME_TH: int = 300000
|
||||
FAST_PING_INTERVAL: int = 5000
|
||||
CONSTRUCT_STATE_PENDING: int = 15000
|
||||
KA_INTERVAL_MIN = 10000
|
||||
@ -472,6 +473,8 @@ class MIoTLan:
|
||||
OT_PROBE_INTERVAL_MIN: int = 5000
|
||||
OT_PROBE_INTERVAL_MAX: int = 45000
|
||||
|
||||
PROFILE_MODELS_FILE: str = 'lan/profile_models.yaml'
|
||||
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
_net_ifs: set[str]
|
||||
_network: MIoTNetwork
|
||||
@ -502,6 +505,8 @@ class MIoTLan:
|
||||
_lan_state_sub_map: dict[str, Callable[[bool], asyncio.Future]]
|
||||
_lan_ctrl_vote_map: dict[str, bool]
|
||||
|
||||
_profile_models: dict[str, dict]
|
||||
|
||||
_init_done: bool
|
||||
|
||||
def __init__(
|
||||
@ -597,6 +602,12 @@ class MIoTLan:
|
||||
if self._net_ifs.isdisjoint(self._available_net_ifs):
|
||||
_LOGGER.info('no valid net_ifs')
|
||||
return
|
||||
try:
|
||||
self._profile_models = load_yaml_file(
|
||||
yaml_file=gen_absolute_path(self.PROFILE_MODELS_FILE))
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.error('load profile models error, %s', err)
|
||||
self._profile_models = {}
|
||||
self._mev = MIoTEventLoop()
|
||||
self._queue = queue.Queue()
|
||||
self._cmd_event_fd = os.eventfd(0, os.O_NONBLOCK)
|
||||
@ -620,6 +631,7 @@ class MIoTLan:
|
||||
self.__lan_send_cmd(MIoTLanCmdType.DEINIT, None)
|
||||
self._thread.join()
|
||||
|
||||
self._profile_models = {}
|
||||
self._lan_devices = {}
|
||||
self._broadcast_socks = {}
|
||||
self._local_port = None
|
||||
@ -1032,6 +1044,19 @@ class MIoTLan:
|
||||
elif mips_cmd.type_ == MIoTLanCmdType.DEVICE_UPDATE:
|
||||
devices: dict[str, dict] = mips_cmd.data
|
||||
for did, info in devices.items():
|
||||
# did MUST be digit(UINT64)
|
||||
if not did.isdigit():
|
||||
_LOGGER.info('invalid did, %s', did)
|
||||
continue
|
||||
if (
|
||||
'model' not in info
|
||||
or info['model'] in self._profile_models):
|
||||
# Do not support the local control of
|
||||
# Profile device for the time being
|
||||
_LOGGER.info(
|
||||
'model not support local ctrl, %s, %s',
|
||||
did, info.get('model'))
|
||||
continue
|
||||
if did not in self._lan_devices:
|
||||
if 'token' not in info:
|
||||
_LOGGER.error(
|
||||
|
@ -43,6 +43,13 @@ def load_py_file():
|
||||
dst=path.join(TEST_FILES_PATH, 'specs'),
|
||||
dirs_exist_ok=True)
|
||||
print('loaded spec test folder, specs')
|
||||
# Copy lan files to test folder
|
||||
shutil.copytree(
|
||||
src=path.join(
|
||||
TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/lan'),
|
||||
dst=path.join(TEST_FILES_PATH, 'lan'),
|
||||
dirs_exist_ok=True)
|
||||
print('loaded lan test folder, lan')
|
||||
# Copy i18n files to test folder
|
||||
shutil.copytree(
|
||||
src=path.join(
|
||||
|
@ -8,8 +8,37 @@ from zeroconf.asyncio import AsyncZeroconf
|
||||
# pylint: disable=import-outside-toplevel, unused-argument
|
||||
|
||||
|
||||
@pytest.mark.parametrize('test_devices', [{
|
||||
# specv2 model
|
||||
'123456': {
|
||||
'token': '11223344556677d9a03d43936fc384205',
|
||||
'model': 'xiaomi.gateway.hub1'
|
||||
},
|
||||
# profile model
|
||||
'123457': {
|
||||
'token': '11223344556677d9a03d43936fc384205',
|
||||
'model': 'yeelink.light.lamp9'
|
||||
},
|
||||
'123458': {
|
||||
'token': '11223344556677d9a03d43936fc384205',
|
||||
'model': 'zhimi.heater.ma1'
|
||||
},
|
||||
# Non -digital did
|
||||
'group.123456': {
|
||||
'token': '11223344556677d9a03d43936fc384205',
|
||||
'model': 'mijia.light.group3'
|
||||
},
|
||||
'proxy.123456.1': {
|
||||
'token': '11223344556677d9a03d43936fc384205',
|
||||
'model': 'xiaomi.light.p1'
|
||||
},
|
||||
'miwifi_123456': {
|
||||
'token': '11223344556677d9a03d43936fc384205',
|
||||
'model': 'xiaomi.light.p1'
|
||||
}
|
||||
}])
|
||||
@pytest.mark.asyncio
|
||||
async def test_lan_async():
|
||||
async def test_lan_async(test_devices: dict):
|
||||
"""
|
||||
Use the central hub gateway as a test equipment, and through the local area
|
||||
network control central hub gateway indicator light switch. Please replace
|
||||
@ -21,10 +50,13 @@ async def test_lan_async():
|
||||
from miot.miot_lan import MIoTLan
|
||||
from miot.miot_mdns import MipsService
|
||||
|
||||
test_did = '<Your central hub gateway did>'
|
||||
test_token = '<Your central hub gateway token>'
|
||||
# Your central hub gateway did
|
||||
test_did = '111111'
|
||||
# Your central hub gateway did
|
||||
test_token = '11223344556677d9a03d43936fc384205'
|
||||
test_model = 'xiaomi.gateway.hub1'
|
||||
test_if_names = ['<Your computer interface list, such as enp3s0, wlp5s0>']
|
||||
# Your computer interface list, such as enp3s0, wlp5s0
|
||||
test_if_names = ['enp3s0', 'wlp5s0']
|
||||
|
||||
# Check test params
|
||||
assert int(test_did) > 0
|
||||
@ -76,7 +108,8 @@ async def test_lan_async():
|
||||
test_did: {
|
||||
'token': test_token,
|
||||
'model': test_model
|
||||
}
|
||||
},
|
||||
**test_devices
|
||||
})
|
||||
|
||||
# Test sub device state
|
||||
|
44
tools/common.py
Normal file
44
tools/common.py
Normal file
@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Common functions."""
|
||||
import json
|
||||
import yaml
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
|
||||
def load_yaml_file(yaml_file: str) -> dict:
|
||||
with open(yaml_file, 'r', encoding='utf-8') as file:
|
||||
return yaml.safe_load(file)
|
||||
|
||||
|
||||
def save_yaml_file(yaml_file: str, data: dict) -> None:
|
||||
with open(yaml_file, 'w', encoding='utf-8') as file:
|
||||
yaml.safe_dump(
|
||||
data=data, stream=file, allow_unicode=True)
|
||||
|
||||
|
||||
def load_json_file(json_file: str) -> dict:
|
||||
with open(json_file, 'r', encoding='utf-8') as file:
|
||||
return json.load(file)
|
||||
|
||||
|
||||
def save_json_file(json_file: str, data: dict) -> None:
|
||||
with open(json_file, 'w', encoding='utf-8') as file:
|
||||
json.dump(data, file, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
def http_get(
|
||||
url: str, params: dict = None, headers: dict = None
|
||||
) -> dict:
|
||||
if params:
|
||||
encoded_params = urlencode(params)
|
||||
full_url = f'{url}?{encoded_params}'
|
||||
else:
|
||||
full_url = url
|
||||
request = Request(full_url, method='GET', headers=headers or {})
|
||||
content: bytes = None
|
||||
with urlopen(request) as response:
|
||||
content = response.read()
|
||||
return (
|
||||
json.loads(str(content, 'utf-8'))
|
||||
if content is not None else None)
|
80
tools/update_lan_rule.py
Normal file
80
tools/update_lan_rule.py
Normal file
@ -0,0 +1,80 @@
|
||||
""" Update LAN rule."""
|
||||
# -*- coding: utf-8 -*-
|
||||
# pylint: disable=relative-beyond-top-level
|
||||
from os import path
|
||||
from common import (
|
||||
http_get,
|
||||
load_yaml_file,
|
||||
save_yaml_file)
|
||||
|
||||
|
||||
ROOT_PATH: str = path.dirname(path.abspath(__file__))
|
||||
LAN_PROFILE_MODELS_FILE: str = path.join(
|
||||
ROOT_PATH,
|
||||
'../custom_components/xiaomi_home/miot/lan/profile_models.yaml')
|
||||
|
||||
|
||||
SPECIAL_MODELS: list[str] = [
|
||||
# model2class-v2
|
||||
'chuangmi.camera.ipc007b', 'chuangmi.camera.ipc019b',
|
||||
'chuangmi.camera.ipc019e', 'chuangmi.camera.ipc020',
|
||||
'chuangmi.camera.v2', 'chuangmi.camera.v5',
|
||||
'chuangmi.camera.v6', 'chuangmi.camera.xiaobai',
|
||||
'chuangmi.radio.v1', 'chuangmi.radio.v2',
|
||||
'hith.foot_bath.q2', 'imou99.camera.tp2',
|
||||
'isa.camera.hl5', 'isa.camera.isc5',
|
||||
'jiqid.mistory.pro', 'jiqid.mistory.v1',
|
||||
'lumi.airrtc.tcpco2ecn01', 'lumi.airrtc.tcpecn02',
|
||||
'lumi.camera.gwagl01', 'miir.light.ir01',
|
||||
'miir.projector.ir01', 'miir.tv.hir01',
|
||||
'miir.tvbox.ir01', 'roome.bhf_light.yf6002',
|
||||
'smith.waterpuri.jnt600', 'viomi.fridge.u2',
|
||||
'xiaovv.camera.lamp', 'xiaovv.camera.ptz',
|
||||
'xiaovv.camera.xva3', 'xiaovv.camera.xvb4',
|
||||
'xiaovv.camera.xvsnowman', 'zdeer.ajh.a8',
|
||||
'zdeer.ajh.a9', 'zdeer.ajh.zda10',
|
||||
'zdeer.ajh.zda9', 'zdeer.ajh.zjy', 'zimi.clock.myk01',
|
||||
# specialModels
|
||||
'chuangmi.camera.ipc004b', 'chuangmi.camera.ipc009',
|
||||
'chuangmi.camera.ipc010', 'chuangmi.camera.ipc013',
|
||||
'chuangmi.camera.ipc013d', 'chuangmi.camera.ipc016',
|
||||
'chuangmi.camera.ipc017', 'chuangmi.camera.ipc019',
|
||||
'chuangmi.camera.ipc021', 'chuangmi.camera.v3',
|
||||
'chuangmi.camera.v4', 'isa.camera.df3',
|
||||
'isa.camera.hlc6', 'lumi.acpartner.v1',
|
||||
'lumi.acpartner.v2', 'lumi.acpartner.v3',
|
||||
'lumi.airrtc.tcpecn01', 'lumi.camera.aq1',
|
||||
'miir.aircondition.ir01', 'miir.aircondition.ir02',
|
||||
'miir.fan.ir01', 'miir.stb.ir01',
|
||||
'miir.tv.ir01', 'mijia.camera.v1',
|
||||
'mijia.camera.v3', 'roborock.sweeper.s5v2',
|
||||
'roborock.vacuum.c1', 'roborock.vacuum.e2',
|
||||
'roborock.vacuum.m1s', 'roborock.vacuum.s5',
|
||||
'rockrobo.vacuum.v1', 'xiaovv.camera.xvd5']
|
||||
|
||||
|
||||
def update_profile_model(file_path: str):
|
||||
profile_rules: dict = http_get(
|
||||
url='https://miot-spec.org/instance/translate/models')
|
||||
if not profile_rules and 'models' not in profile_rules and not isinstance(
|
||||
profile_rules['models'], dict):
|
||||
raise ValueError('Failed to get profile rule')
|
||||
local_rules: dict = load_yaml_file(
|
||||
yaml_file=file_path) or {}
|
||||
for rule, ts in profile_rules['models'].items():
|
||||
if rule not in local_rules:
|
||||
local_rules[rule] = {'ts': ts}
|
||||
else:
|
||||
local_rules[rule]['ts'] = ts
|
||||
for mode in SPECIAL_MODELS:
|
||||
if mode not in local_rules:
|
||||
local_rules[mode] = {'ts': 1531108800}
|
||||
else:
|
||||
local_rules[mode]['ts'] = 1531108800
|
||||
local_rules = dict(sorted(local_rules.items()))
|
||||
save_yaml_file(
|
||||
yaml_file=file_path, data=local_rules)
|
||||
|
||||
|
||||
update_profile_model(file_path=LAN_PROFILE_MODELS_FILE)
|
||||
print('profile model list updated.')
|
Loading…
x
Reference in New Issue
Block a user