mirror of
https://github.com/XiaoMi/ha_xiaomi_home.git
synced 2025-03-31 14:55:31 +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.
|
Common utilities.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
from os import path
|
||||||
import random
|
import random
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import hashlib
|
import hashlib
|
||||||
from paho.mqtt.client import MQTTMatcher
|
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:
|
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)
|
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:
|
def randomize_int(value: int, ratio: float) -> int:
|
||||||
"""Randomize an integer value."""
|
"""Randomize an integer value."""
|
||||||
return int(value * (1 - ratio + random.random()*2*ratio))
|
return int(value * (1 - ratio + random.random()*2*ratio))
|
||||||
@ -74,12 +89,12 @@ class MIoTMatcher(MQTTMatcher):
|
|||||||
|
|
||||||
def iter_all_nodes(self) -> any:
|
def iter_all_nodes(self) -> any:
|
||||||
"""Return an iterator on all nodes with their paths and contents."""
|
"""Return an iterator on all nodes with their paths and contents."""
|
||||||
def rec(node, path):
|
def rec(node, path_):
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
if node._content:
|
if node._content:
|
||||||
yield ('/'.join(path), node._content)
|
yield ('/'.join(path_), node._content)
|
||||||
for part, child in node._children.items():
|
for part, child in node._children.items():
|
||||||
yield from rec(child, path + [part])
|
yield from rec(child, path_ + [part])
|
||||||
return rec(self._root, [])
|
return rec(self._root, [])
|
||||||
|
|
||||||
def get(self, topic: str) -> Optional[any]:
|
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_ev import MIoTEventLoop, TimeoutHandle
|
||||||
from .miot_network import InterfaceStatus, MIoTNetwork, NetworkInfo
|
from .miot_network import InterfaceStatus, MIoTNetwork, NetworkInfo
|
||||||
from .miot_mdns import MipsService, MipsServiceState
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -175,7 +176,7 @@ class MIoTLanDevice:
|
|||||||
OT_HEADER_LEN: int = 32
|
OT_HEADER_LEN: int = 32
|
||||||
NETWORK_UNSTABLE_CNT_TH: int = 10
|
NETWORK_UNSTABLE_CNT_TH: int = 10
|
||||||
NETWORK_UNSTABLE_TIME_TH: int = 120000
|
NETWORK_UNSTABLE_TIME_TH: int = 120000
|
||||||
NETWORK_UNSTABLE_RESUME_TH: int = 300
|
NETWORK_UNSTABLE_RESUME_TH: int = 300000
|
||||||
FAST_PING_INTERVAL: int = 5000
|
FAST_PING_INTERVAL: int = 5000
|
||||||
CONSTRUCT_STATE_PENDING: int = 15000
|
CONSTRUCT_STATE_PENDING: int = 15000
|
||||||
KA_INTERVAL_MIN = 10000
|
KA_INTERVAL_MIN = 10000
|
||||||
@ -472,6 +473,8 @@ class MIoTLan:
|
|||||||
OT_PROBE_INTERVAL_MIN: int = 5000
|
OT_PROBE_INTERVAL_MIN: int = 5000
|
||||||
OT_PROBE_INTERVAL_MAX: int = 45000
|
OT_PROBE_INTERVAL_MAX: int = 45000
|
||||||
|
|
||||||
|
PROFILE_MODELS_FILE: str = 'lan/profile_models.yaml'
|
||||||
|
|
||||||
_main_loop: asyncio.AbstractEventLoop
|
_main_loop: asyncio.AbstractEventLoop
|
||||||
_net_ifs: set[str]
|
_net_ifs: set[str]
|
||||||
_network: MIoTNetwork
|
_network: MIoTNetwork
|
||||||
@ -502,6 +505,8 @@ class MIoTLan:
|
|||||||
_lan_state_sub_map: dict[str, Callable[[bool], asyncio.Future]]
|
_lan_state_sub_map: dict[str, Callable[[bool], asyncio.Future]]
|
||||||
_lan_ctrl_vote_map: dict[str, bool]
|
_lan_ctrl_vote_map: dict[str, bool]
|
||||||
|
|
||||||
|
_profile_models: dict[str, dict]
|
||||||
|
|
||||||
_init_done: bool
|
_init_done: bool
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -597,6 +602,12 @@ class MIoTLan:
|
|||||||
if self._net_ifs.isdisjoint(self._available_net_ifs):
|
if self._net_ifs.isdisjoint(self._available_net_ifs):
|
||||||
_LOGGER.info('no valid net_ifs')
|
_LOGGER.info('no valid net_ifs')
|
||||||
return
|
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._mev = MIoTEventLoop()
|
||||||
self._queue = queue.Queue()
|
self._queue = queue.Queue()
|
||||||
self._cmd_event_fd = os.eventfd(0, os.O_NONBLOCK)
|
self._cmd_event_fd = os.eventfd(0, os.O_NONBLOCK)
|
||||||
@ -620,6 +631,7 @@ class MIoTLan:
|
|||||||
self.__lan_send_cmd(MIoTLanCmdType.DEINIT, None)
|
self.__lan_send_cmd(MIoTLanCmdType.DEINIT, None)
|
||||||
self._thread.join()
|
self._thread.join()
|
||||||
|
|
||||||
|
self._profile_models = {}
|
||||||
self._lan_devices = {}
|
self._lan_devices = {}
|
||||||
self._broadcast_socks = {}
|
self._broadcast_socks = {}
|
||||||
self._local_port = None
|
self._local_port = None
|
||||||
@ -1032,6 +1044,19 @@ class MIoTLan:
|
|||||||
elif mips_cmd.type_ == MIoTLanCmdType.DEVICE_UPDATE:
|
elif mips_cmd.type_ == MIoTLanCmdType.DEVICE_UPDATE:
|
||||||
devices: dict[str, dict] = mips_cmd.data
|
devices: dict[str, dict] = mips_cmd.data
|
||||||
for did, info in devices.items():
|
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 did not in self._lan_devices:
|
||||||
if 'token' not in info:
|
if 'token' not in info:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
|
@ -43,6 +43,13 @@ def load_py_file():
|
|||||||
dst=path.join(TEST_FILES_PATH, 'specs'),
|
dst=path.join(TEST_FILES_PATH, 'specs'),
|
||||||
dirs_exist_ok=True)
|
dirs_exist_ok=True)
|
||||||
print('loaded spec test folder, specs')
|
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
|
# Copy i18n files to test folder
|
||||||
shutil.copytree(
|
shutil.copytree(
|
||||||
src=path.join(
|
src=path.join(
|
||||||
|
@ -8,8 +8,37 @@ from zeroconf.asyncio import AsyncZeroconf
|
|||||||
# pylint: disable=import-outside-toplevel, unused-argument
|
# 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
|
@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
|
Use the central hub gateway as a test equipment, and through the local area
|
||||||
network control central hub gateway indicator light switch. Please replace
|
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_lan import MIoTLan
|
||||||
from miot.miot_mdns import MipsService
|
from miot.miot_mdns import MipsService
|
||||||
|
|
||||||
test_did = '<Your central hub gateway did>'
|
# Your central hub gateway did
|
||||||
test_token = '<Your central hub gateway token>'
|
test_did = '111111'
|
||||||
|
# Your central hub gateway did
|
||||||
|
test_token = '11223344556677d9a03d43936fc384205'
|
||||||
test_model = 'xiaomi.gateway.hub1'
|
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
|
# Check test params
|
||||||
assert int(test_did) > 0
|
assert int(test_did) > 0
|
||||||
@ -76,7 +108,8 @@ async def test_lan_async():
|
|||||||
test_did: {
|
test_did: {
|
||||||
'token': test_token,
|
'token': test_token,
|
||||||
'model': test_model
|
'model': test_model
|
||||||
}
|
},
|
||||||
|
**test_devices
|
||||||
})
|
})
|
||||||
|
|
||||||
# Test sub device state
|
# 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