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:
Paul Shawn 2024-12-20 19:21:43 +08:00 committed by GitHub
parent 6ce3206b30
commit bd3a98b976
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 1534 additions and 10 deletions

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -8,8 +8,37 @@ from zeroconf.asyncio import AsyncZeroconf
# pylint: disable=import-outside-toplevel, unused-argument
@pytest.mark.parametrize('test_devices', [{
# specv2 model
'123456': {
'token': '11223344556677d9a03d43936fc384205',
'model': 'xiaomi.gateway.hub1'
},
# profile model
'123457': {
'token': '11223344556677d9a03d43936fc384205',
'model': 'yeelink.light.lamp9'
},
'123458': {
'token': '11223344556677d9a03d43936fc384205',
'model': 'zhimi.heater.ma1'
},
# Non -digital did
'group.123456': {
'token': '11223344556677d9a03d43936fc384205',
'model': 'mijia.light.group3'
},
'proxy.123456.1': {
'token': '11223344556677d9a03d43936fc384205',
'model': 'xiaomi.light.p1'
},
'miwifi_123456': {
'token': '11223344556677d9a03d43936fc384205',
'model': 'xiaomi.light.p1'
}
}])
@pytest.mark.asyncio
async def test_lan_async():
async def test_lan_async(test_devices: dict):
"""
Use the central hub gateway as a test equipment, and through the local area
network control central hub gateway indicator light switch. Please replace
@ -21,10 +50,13 @@ async def test_lan_async():
from miot.miot_lan import MIoTLan
from miot.miot_mdns import MipsService
test_did = '<Your central hub gateway did>'
test_token = '<Your central hub gateway token>'
# Your central hub gateway did
test_did = '111111'
# Your central hub gateway did
test_token = '11223344556677d9a03d43936fc384205'
test_model = 'xiaomi.gateway.hub1'
test_if_names = ['<Your computer interface list, such as enp3s0, wlp5s0>']
# Your computer interface list, such as enp3s0, wlp5s0
test_if_names = ['enp3s0', 'wlp5s0']
# Check test params
assert int(test_did) > 0
@ -76,7 +108,8 @@ async def test_lan_async():
test_did: {
'token': test_token,
'model': test_model
}
},
**test_devices
})
# Test sub device state

44
tools/common.py Normal file
View 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
View 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.')