mirror of
https://github.com/XiaoMi/ha_xiaomi_home.git
synced 2025-03-31 14:55:31 +08:00
1807 lines
64 KiB
Python
1807 lines
64 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright (C) 2024 Xiaomi Corporation.
|
|
|
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
|
Integration and related Xiaomi cloud service API interface provided under this
|
|
license, including source code and object code (collectively, "Licensed Work"),
|
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
|
distribute the Licensed Work only for your use of Home Assistant for
|
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
|
you to use the Licensed Work for any other purpose, including but not limited
|
|
to use Licensed Work to develop applications (APP), Web services, and other
|
|
forms of software.
|
|
|
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
|
modifications, whether in source or object form, provided that you must give
|
|
any other recipients of the Licensed Work a copy of this License and retain all
|
|
copyright and disclaimers.
|
|
|
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
|
for any direct, indirect, special, incidental, or consequential damages or
|
|
losses arising from the use or inability to use the Licensed Work.
|
|
|
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
|
without limitation, without obtaining other written permission from Xiaomi, you
|
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
|
may make the public associate with Xiaomi in any form to publicize or promote
|
|
the software or hardware devices that use the Licensed Work.
|
|
|
|
Xiaomi has the right to immediately terminate all your authorization under this
|
|
License in the event:
|
|
1. You assert patent invalidation, litigation, or other claims against patents
|
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
|
off Xiaomi or its affiliates' products.
|
|
|
|
MIoT Pub/Sub client.
|
|
"""
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import os
|
|
import queue
|
|
import random
|
|
import re
|
|
import ssl
|
|
import struct
|
|
import threading
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass
|
|
from enum import Enum, auto
|
|
from typing import Callable, Optional, final
|
|
|
|
from paho.mqtt.client import (
|
|
MQTT_ERR_SUCCESS,
|
|
MQTT_ERR_UNKNOWN,
|
|
Client,
|
|
MQTTv5)
|
|
|
|
from .common import MIoTMatcher
|
|
from .const import MIHOME_MQTT_KEEPALIVE
|
|
from .miot_error import MIoTErrorCode, MIoTMipsError
|
|
from .miot_ev import MIoTEventLoop, TimeoutHandle
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class MipsMsgTypeOptions(Enum):
|
|
"""MIoT Pub/Sub message type."""
|
|
ID = 0
|
|
RET_TOPIC = auto()
|
|
PAYLOAD = auto()
|
|
FROM = auto()
|
|
MAX = auto()
|
|
|
|
|
|
class MipsMessage:
|
|
"""MIoT Pub/Sub message."""
|
|
mid: int = 0
|
|
msg_from: str = None
|
|
ret_topic: str = None
|
|
payload: str = None
|
|
|
|
@staticmethod
|
|
def unpack(data: bytes):
|
|
mips_msg = MipsMessage()
|
|
data_len = len(data)
|
|
data_start = 0
|
|
data_end = 0
|
|
while data_start < data_len:
|
|
data_end = data_start+5
|
|
unpack_len, unpack_type = struct.unpack(
|
|
'<IB', data[data_start:data_end])
|
|
unpack_data = data[data_end:data_end+unpack_len]
|
|
# string end with \x00
|
|
match unpack_type:
|
|
case MipsMsgTypeOptions.ID.value:
|
|
mips_msg.mid = int.from_bytes(
|
|
unpack_data, byteorder='little')
|
|
case MipsMsgTypeOptions.RET_TOPIC.value:
|
|
mips_msg.ret_topic = str(
|
|
unpack_data.strip(b'\x00'), 'utf-8')
|
|
case MipsMsgTypeOptions.PAYLOAD.value:
|
|
mips_msg.payload = str(unpack_data.strip(b'\x00'), 'utf-8')
|
|
case MipsMsgTypeOptions.FROM.value:
|
|
mips_msg.msg_from = str(
|
|
unpack_data.strip(b'\x00'), 'utf-8')
|
|
case _:
|
|
pass
|
|
data_start = data_end+unpack_len
|
|
return mips_msg
|
|
|
|
@staticmethod
|
|
def pack(
|
|
mid: int, payload: str, msg_from: str = None, ret_topic: str = None
|
|
) -> bytes:
|
|
if mid is None or payload is None:
|
|
raise MIoTMipsError('invalid mid or payload')
|
|
pack_msg: bytes = b''
|
|
# mid
|
|
pack_msg += struct.pack('<IBI', 4, MipsMsgTypeOptions.ID.value, mid)
|
|
# msg_from
|
|
if msg_from:
|
|
pack_len = len(msg_from)
|
|
pack_msg += struct.pack(
|
|
f'<IB{pack_len}sx', pack_len+1,
|
|
MipsMsgTypeOptions.FROM.value, msg_from.encode('utf-8'))
|
|
# ret_topic
|
|
if ret_topic:
|
|
pack_len = len(ret_topic)
|
|
pack_msg += struct.pack(
|
|
f'<IB{pack_len}sx', pack_len+1,
|
|
MipsMsgTypeOptions.RET_TOPIC.value, ret_topic.encode('utf-8'))
|
|
# payload
|
|
pack_len = len(payload)
|
|
pack_msg += struct.pack(
|
|
f'<IB{pack_len}sx', pack_len+1,
|
|
MipsMsgTypeOptions.PAYLOAD.value, payload.encode('utf-8'))
|
|
return pack_msg
|
|
|
|
def __str__(self) -> str:
|
|
return f'{self.mid}, {self.msg_from}, {self.ret_topic}, {self.payload}'
|
|
|
|
|
|
class MipsCmdType(Enum):
|
|
"""MIoT Pub/Sub command type."""
|
|
CONNECT = 0
|
|
DISCONNECT = auto()
|
|
DEINIT = auto()
|
|
SUB = auto()
|
|
UNSUB = auto()
|
|
CALL_API = auto()
|
|
REG_BROADCAST = auto()
|
|
UNREG_BROADCAST = auto()
|
|
|
|
REG_MIPS_STATE = auto()
|
|
UNREG_MIPS_STATE = auto()
|
|
REG_DEVICE_STATE = auto()
|
|
UNREG_DEVICE_STATE = auto()
|
|
|
|
|
|
@dataclass
|
|
class MipsCmd:
|
|
"""MIoT Pub/Sub command."""
|
|
type_: MipsCmdType
|
|
data: any
|
|
|
|
def __init__(self, type_: MipsCmdType, data: any) -> None:
|
|
self.type_ = type_
|
|
self.data = data
|
|
|
|
|
|
@dataclass
|
|
class MipsRequest:
|
|
"""MIoT Pub/Sub request."""
|
|
mid: int = None
|
|
on_reply: Callable[[str, any], None] = None
|
|
on_reply_ctx: any = None
|
|
timer: TimeoutHandle = None
|
|
|
|
|
|
@dataclass
|
|
class MipsRequestData:
|
|
"""MIoT Pub/Sub request data."""
|
|
topic: str = None
|
|
payload: str = None
|
|
on_reply: Callable[[str, any], None] = None
|
|
on_reply_ctx: any = None
|
|
timeout_ms: int = None
|
|
|
|
|
|
@dataclass
|
|
class MipsSendBroadcastData:
|
|
"""MIoT Pub/Sub send broadcast data."""
|
|
topic: str = None
|
|
payload: str = None
|
|
|
|
|
|
@dataclass
|
|
class MipsIncomingApiCall:
|
|
"""MIoT Pub/Sub incoming API call."""
|
|
mid: int = None
|
|
ret_topic: str = None
|
|
timer: TimeoutHandle = None
|
|
|
|
|
|
@dataclass
|
|
class MipsApi:
|
|
"""MIoT Pub/Sub API."""
|
|
topic: str = None
|
|
"""
|
|
param1: session
|
|
param2: payload
|
|
param3: handler_ctx
|
|
"""
|
|
handler: Callable[[MipsIncomingApiCall, str, any], None] = None
|
|
handler_ctx: any = None
|
|
|
|
|
|
class MipsRegApi(MipsApi):
|
|
""".MIoT Pub/Sub register API."""
|
|
|
|
|
|
@dataclass
|
|
class MipsReplyData:
|
|
"""MIoT Pub/Sub reply data."""
|
|
session: MipsIncomingApiCall = None
|
|
payload: str = None
|
|
|
|
|
|
@dataclass
|
|
class MipsBroadcast:
|
|
"""MIoT Pub/Sub broadcast."""
|
|
topic: str = None
|
|
"""
|
|
param 1: msg topic
|
|
param 2: msg payload
|
|
param 3: handle_ctx
|
|
"""
|
|
handler: Callable[[str, str, any], None] = None
|
|
handler_ctx: any = None
|
|
|
|
def __str__(self) -> str:
|
|
return f'{self.topic}, {id(self.handler)}, {id(self.handler_ctx)}'
|
|
|
|
|
|
class MipsRegBroadcast(MipsBroadcast):
|
|
"""MIoT Pub/Sub register broadcast."""
|
|
|
|
|
|
@dataclass
|
|
class MipsState:
|
|
"""MIoT Pub/Sub state."""
|
|
key: str = None
|
|
"""
|
|
str: key
|
|
bool: mips connect state
|
|
any: ctx
|
|
"""
|
|
handler: Callable[[str, bool], asyncio.Future] = None
|
|
|
|
|
|
class MipsRegState(MipsState):
|
|
"""MIoT Pub/Sub register state."""
|
|
|
|
|
|
class MIoTDeviceState(Enum):
|
|
"""MIoT device state define."""
|
|
DISABLE = 0
|
|
OFFLINE = auto()
|
|
ONLINE = auto()
|
|
|
|
|
|
@dataclass
|
|
class MipsDeviceState:
|
|
"""MIoT Pub/Sub device state."""
|
|
did: str = None
|
|
"""handler
|
|
str: did
|
|
MIoTDeviceState: online/offline/disable
|
|
any: ctx
|
|
"""
|
|
handler: Callable[[str, MIoTDeviceState, any], None] = None
|
|
handler_ctx: any = None
|
|
|
|
|
|
class MipsRegDeviceState(MipsDeviceState):
|
|
"""MIoT Pub/Sub register device state."""
|
|
|
|
|
|
class MipsClient(ABC):
|
|
"""MIoT Pub/Sub client."""
|
|
# pylint: disable=unused-argument
|
|
MQTT_INTERVAL_MS = 1000
|
|
MIPS_QOS: int = 2
|
|
UINT32_MAX: int = 0xFFFFFFFF
|
|
MIPS_RECONNECT_INTERVAL_MIN: int = 30000
|
|
MIPS_RECONNECT_INTERVAL_MAX: int = 600000
|
|
MIPS_SUB_PATCH: int = 300
|
|
MIPS_SUB_INTERVAL: int = 1000
|
|
main_loop: asyncio.AbstractEventLoop
|
|
_logger: logging.Logger
|
|
_client_id: str
|
|
_host: str
|
|
_port: int
|
|
_username: str
|
|
_password: str
|
|
_ca_file: str
|
|
_cert_file: str
|
|
_key_file: str
|
|
|
|
_mqtt_logger: logging.Logger
|
|
_mqtt: Client
|
|
_mqtt_fd: int
|
|
_mqtt_timer: TimeoutHandle
|
|
_mqtt_state: bool
|
|
|
|
_event_connect: asyncio.Event
|
|
_event_disconnect: asyncio.Event
|
|
_mev: MIoTEventLoop
|
|
_mips_thread: threading.Thread
|
|
_mips_queue: queue.Queue
|
|
_cmd_event_fd: os.eventfd
|
|
_mips_reconnect_tag: bool
|
|
_mips_reconnect_interval: int
|
|
_mips_reconnect_timer: Optional[TimeoutHandle]
|
|
_mips_state_sub_map: dict[str, MipsState]
|
|
_mips_sub_pending_map: dict[str, int]
|
|
_mips_sub_pending_timer: Optional[TimeoutHandle]
|
|
|
|
_on_mips_cmd: Callable[[MipsCmd], None]
|
|
_on_mips_message: Callable[[str, bytes], None]
|
|
_on_mips_connect: Callable[[int, dict], None]
|
|
_on_mips_disconnect: Callable[[int, dict], None]
|
|
|
|
def __init__(
|
|
self, client_id: str, host: str, port: int,
|
|
username: str = None, password: str = None,
|
|
ca_file: str = None, cert_file: str = None, key_file: str = None,
|
|
loop: Optional[asyncio.AbstractEventLoop] = None
|
|
) -> None:
|
|
# MUST run with running loop
|
|
self.main_loop = loop or asyncio.get_running_loop()
|
|
self._logger = None
|
|
self._client_id = client_id
|
|
self._host = host
|
|
self._port = port
|
|
self._username = username
|
|
self._password = password
|
|
self._ca_file = ca_file
|
|
self._cert_file = cert_file
|
|
self._key_file = key_file
|
|
|
|
self._mqtt_logger = None
|
|
self._mqtt_fd = -1
|
|
self._mqtt_timer = None
|
|
self._mqtt_state = False
|
|
# mqtt init for API_VERSION2,
|
|
# callback_api_version=CallbackAPIVersion.VERSION2,
|
|
self._mqtt = Client(client_id=self._client_id, protocol=MQTTv5)
|
|
self._mqtt.enable_logger(logger=self._mqtt_logger)
|
|
|
|
# Mips init
|
|
self._event_connect = asyncio.Event()
|
|
self._event_disconnect = asyncio.Event()
|
|
self._mips_reconnect_tag = False
|
|
self._mips_reconnect_interval = 0
|
|
self._mips_reconnect_timer = None
|
|
self._mips_state_sub_map = {}
|
|
self._mips_sub_pending_map = {}
|
|
self._mips_sub_pending_timer = None
|
|
self._mev = MIoTEventLoop()
|
|
self._mips_queue = queue.Queue()
|
|
self._cmd_event_fd = os.eventfd(0, os.O_NONBLOCK)
|
|
self.mev_set_read_handler(
|
|
self._cmd_event_fd, self.__mips_cmd_read_handler, None)
|
|
self._mips_thread = threading.Thread(target=self.__mips_loop_thread)
|
|
self._mips_thread.daemon = True
|
|
self._mips_thread.name = self._client_id
|
|
self._mips_thread.start()
|
|
|
|
self._on_mips_cmd = None
|
|
self._on_mips_message = None
|
|
self._on_mips_connect = None
|
|
self._on_mips_disconnect = None
|
|
|
|
@property
|
|
def client_id(self) -> str:
|
|
return self._client_id
|
|
|
|
@property
|
|
def host(self) -> str:
|
|
return self._host
|
|
|
|
@property
|
|
def port(self) -> int:
|
|
return self._port
|
|
|
|
@final
|
|
@property
|
|
def mips_state(self) -> bool:
|
|
"""mips connect state.
|
|
|
|
Returns:
|
|
bool: True: connected, False: disconnected
|
|
"""
|
|
return self._mqtt and self._mqtt.is_connected()
|
|
|
|
@final
|
|
def mips_deinit(self) -> None:
|
|
self._mips_send_cmd(type_=MipsCmdType.DEINIT, data=None)
|
|
self._mips_thread.join()
|
|
self._mips_thread = None
|
|
|
|
self._logger = None
|
|
self._client_id = None
|
|
self._host = None
|
|
self._port = None
|
|
self._username = None
|
|
self._password = None
|
|
self._ca_file = None
|
|
self._cert_file = None
|
|
self._key_file = None
|
|
self._mqtt_logger = None
|
|
self._mips_state_sub_map = None
|
|
self._mips_sub_pending_map = None
|
|
self._mips_sub_pending_timer = None
|
|
|
|
self._event_connect = None
|
|
self._event_disconnect = None
|
|
|
|
def update_mqtt_password(self, password: str) -> None:
|
|
self._password = password
|
|
self._mqtt.username_pw_set(
|
|
username=self._username, password=self._password)
|
|
|
|
def log_debug(self, msg, *args, **kwargs) -> None:
|
|
if self._logger:
|
|
self._logger.debug(f'{self._client_id}, '+msg, *args, **kwargs)
|
|
|
|
def log_info(self, msg, *args, **kwargs) -> None:
|
|
if self._logger:
|
|
self._logger.info(f'{self._client_id}, '+msg, *args, **kwargs)
|
|
|
|
def log_error(self, msg, *args, **kwargs) -> None:
|
|
if self._logger:
|
|
self._logger.error(f'{self._client_id}, '+msg, *args, **kwargs)
|
|
|
|
def enable_logger(self, logger: Optional[logging.Logger] = None) -> None:
|
|
self._logger = logger
|
|
|
|
def enable_mqtt_logger(
|
|
self, logger: Optional[logging.Logger] = None
|
|
) -> None:
|
|
if logger:
|
|
self._mqtt.enable_logger(logger=logger)
|
|
else:
|
|
self._mqtt.disable_logger()
|
|
|
|
@final
|
|
def mips_connect(self) -> None:
|
|
"""mips connect."""
|
|
return self._mips_send_cmd(type_=MipsCmdType.CONNECT, data=None)
|
|
|
|
@final
|
|
async def mips_connect_async(self) -> None:
|
|
"""mips connect async."""
|
|
self._mips_send_cmd(type_=MipsCmdType.CONNECT, data=None)
|
|
return await self._event_connect.wait()
|
|
|
|
@final
|
|
def mips_disconnect(self) -> None:
|
|
"""mips disconnect."""
|
|
return self._mips_send_cmd(type_=MipsCmdType.DISCONNECT, data=None)
|
|
|
|
@final
|
|
async def mips_disconnect_async(self) -> None:
|
|
"""mips disconnect async."""
|
|
self._mips_send_cmd(type_=MipsCmdType.DISCONNECT, data=None)
|
|
return await self._event_disconnect.wait()
|
|
|
|
@final
|
|
def sub_mips_state(
|
|
self, key: str, handler: Callable[[str, bool], asyncio.Future]
|
|
) -> bool:
|
|
"""Subscribe mips state.
|
|
NOTICE: callback to main loop thread
|
|
"""
|
|
if isinstance(key, str) is False or handler is None:
|
|
raise MIoTMipsError('invalid params')
|
|
return self._mips_send_cmd(
|
|
type_=MipsCmdType.REG_MIPS_STATE,
|
|
data=MipsRegState(key=key, handler=handler))
|
|
|
|
@final
|
|
def unsub_mips_state(self, key: str) -> bool:
|
|
"""Unsubscribe mips state."""
|
|
if isinstance(key, str) is False:
|
|
raise MIoTMipsError('invalid params')
|
|
return self._mips_send_cmd(
|
|
type_=MipsCmdType.UNREG_MIPS_STATE, data=MipsRegState(key=key))
|
|
|
|
@final
|
|
def mev_set_timeout(
|
|
self, timeout_ms: int, handler: Callable[[any], None],
|
|
handler_ctx: any = None
|
|
) -> Optional[TimeoutHandle]:
|
|
"""set timeout.
|
|
NOTICE: Internal function, only mips threads are allowed to call
|
|
"""
|
|
if self._mev is None:
|
|
return None
|
|
return self._mev.set_timeout(
|
|
timeout_ms=timeout_ms, handler=handler, handler_ctx=handler_ctx)
|
|
|
|
@final
|
|
def mev_clear_timeout(self, handle: TimeoutHandle) -> None:
|
|
"""clear timeout.
|
|
NOTICE: Internal function, only mips threads are allowed to call
|
|
"""
|
|
if self._mev is None:
|
|
return
|
|
self._mev.clear_timeout(handle)
|
|
|
|
@final
|
|
def mev_set_read_handler(
|
|
self, fd: int, handler: Callable[[any], None], handler_ctx: any
|
|
) -> bool:
|
|
"""set read handler.
|
|
NOTICE: Internal function, only mips threads are allowed to call
|
|
"""
|
|
if self._mev is None:
|
|
return False
|
|
return self._mev.set_read_handler(
|
|
fd=fd, handler=handler, handler_ctx=handler_ctx)
|
|
|
|
@final
|
|
def mev_set_write_handler(
|
|
self, fd: int, handler: Callable[[any], None], handler_ctx: any
|
|
) -> bool:
|
|
"""set write handler.
|
|
NOTICE: Internal function, only mips threads are allowed to call
|
|
"""
|
|
if self._mev is None:
|
|
return False
|
|
return self._mev.set_write_handler(
|
|
fd=fd, handler=handler, handler_ctx=handler_ctx)
|
|
|
|
@property
|
|
def on_mips_cmd(self) -> Callable[[MipsCmd], None]:
|
|
return self._on_mips_cmd
|
|
|
|
@on_mips_cmd.setter
|
|
def on_mips_cmd(self, handler: Callable[[MipsCmd], None]) -> None:
|
|
"""MUST set after __init__ done.
|
|
NOTICE thread safe, this function will be called at the **mips** thread
|
|
"""
|
|
self._on_mips_cmd = handler
|
|
|
|
@property
|
|
def on_mips_message(self) -> Callable[[str, bytes], None]:
|
|
return self._on_mips_message
|
|
|
|
@on_mips_message.setter
|
|
def on_mips_message(self, handler: Callable[[str, bytes], None]) -> None:
|
|
"""MUST set after __init__ done.
|
|
NOTICE thread safe, this function will be called at the **mips** thread
|
|
"""
|
|
self._on_mips_message = handler
|
|
|
|
@property
|
|
def on_mips_connect(self) -> Callable[[int, dict], None]:
|
|
return self._on_mips_connect
|
|
|
|
@on_mips_connect.setter
|
|
def on_mips_connect(self, handler: Callable[[int, dict], None]) -> None:
|
|
"""MUST set after __init__ done.
|
|
NOTICE thread safe, this function will be called at the
|
|
**main loop** thread
|
|
"""
|
|
self._on_mips_connect = handler
|
|
|
|
@property
|
|
def on_mips_disconnect(self) -> Callable[[int, dict], None]:
|
|
return self._on_mips_disconnect
|
|
|
|
@on_mips_disconnect.setter
|
|
def on_mips_disconnect(self, handler: Callable[[int, dict], None]) -> None:
|
|
"""MUST set after __init__ done.
|
|
NOTICE thread safe, this function will be called at the
|
|
**main loop** thread
|
|
"""
|
|
self._on_mips_disconnect = handler
|
|
|
|
@abstractmethod
|
|
def sub_prop(
|
|
self, did: str, handler: Callable[[dict, any], None],
|
|
siid: int = None, piid: int = None, handler_ctx: any = None
|
|
) -> bool: ...
|
|
|
|
@abstractmethod
|
|
def unsub_prop(
|
|
self, did: str, siid: int = None, piid: int = None
|
|
) -> bool: ...
|
|
|
|
@abstractmethod
|
|
def sub_event(
|
|
self, did: str, handler: Callable[[dict, any], None],
|
|
siid: int = None, eiid: int = None, handler_ctx: any = None
|
|
) -> bool: ...
|
|
|
|
@abstractmethod
|
|
def unsub_event(
|
|
self, did: str, siid: int = None, eiid: int = None
|
|
) -> bool: ...
|
|
|
|
@abstractmethod
|
|
async def get_dev_list_async(
|
|
self, payload: str = None, timeout_ms: int = 10000
|
|
) -> dict[str, dict]: ...
|
|
|
|
@abstractmethod
|
|
async def get_prop_async(
|
|
self, did: str, siid: int, piid: int, timeout_ms: int = 10000
|
|
) -> any: ...
|
|
|
|
@abstractmethod
|
|
async def set_prop_async(
|
|
self, did: str, siid: int, piid: int, value: any,
|
|
timeout_ms: int = 10000
|
|
) -> bool: ...
|
|
|
|
@abstractmethod
|
|
async def action_async(
|
|
self, did: str, siid: int, aiid: int, in_list: list,
|
|
timeout_ms: int = 10000
|
|
) -> tuple[bool, list]: ...
|
|
|
|
@final
|
|
def _mips_sub_internal(self, topic: str) -> None:
|
|
"""mips subscribe.
|
|
NOTICE: Internal function, only mips threads are allowed to call
|
|
"""
|
|
self.__thread_check()
|
|
if not self._mqtt or not self._mqtt.is_connected():
|
|
return
|
|
try:
|
|
if topic not in self._mips_sub_pending_map:
|
|
self._mips_sub_pending_map[topic] = 0
|
|
if not self._mips_sub_pending_timer:
|
|
self._mips_sub_pending_timer = self.mev_set_timeout(
|
|
10, self.__mips_sub_internal_pending_handler, topic)
|
|
except Exception as err: # pylint: disable=broad-exception-caught
|
|
# Catch all exception
|
|
self.log_error(f'mips sub internal error, {topic}. {err}')
|
|
|
|
@final
|
|
def _mips_unsub_internal(self, topic: str) -> None:
|
|
"""mips unsubscribe.
|
|
NOTICE: Internal function, only mips threads are allowed to call
|
|
"""
|
|
self.__thread_check()
|
|
if not self._mqtt or not self._mqtt.is_connected():
|
|
return
|
|
try:
|
|
result, mid = self._mqtt.unsubscribe(topic=topic)
|
|
if result == MQTT_ERR_SUCCESS:
|
|
self.log_debug(
|
|
f'mips unsub internal success, {result}, {mid}, {topic}')
|
|
return
|
|
self.log_error(
|
|
f'mips unsub internal error, {result}, {mid}, {topic}')
|
|
except Exception as err: # pylint: disable=broad-exception-caught
|
|
# Catch all exception
|
|
self.log_error(f'mips unsub internal error, {topic}, {err}')
|
|
|
|
@final
|
|
def _mips_publish_internal(
|
|
self, topic: str, payload: str | bytes,
|
|
wait_for_publish: bool = False, timeout_ms: int = 10000
|
|
) -> bool:
|
|
"""mips publish message.
|
|
NOTICE: Internal function, only mips threads are allowed to call
|
|
|
|
"""
|
|
self.__thread_check()
|
|
if not self._mqtt or not self._mqtt.is_connected():
|
|
return False
|
|
try:
|
|
handle = self._mqtt.publish(
|
|
topic=topic, payload=payload, qos=self.MIPS_QOS)
|
|
# self.log_debug(f'_mips_publish_internal, {topic}, {payload}')
|
|
if wait_for_publish is True:
|
|
handle.wait_for_publish(timeout_ms/1000.0)
|
|
return True
|
|
except Exception as err: # pylint: disable=broad-exception-caught
|
|
# Catch other exception
|
|
self.log_error(f'mips publish internal error, {err}')
|
|
return False
|
|
|
|
@final
|
|
def _mips_send_cmd(self, type_: MipsCmdType, data: any) -> bool:
|
|
if self._mips_queue is None or self._cmd_event_fd is None:
|
|
raise MIoTMipsError('send mips cmd disable')
|
|
# Put data to queue
|
|
self._mips_queue.put(MipsCmd(type_=type_, data=data))
|
|
# Write event fd
|
|
os.eventfd_write(self._cmd_event_fd, 1)
|
|
# self.log_debug(f'send mips cmd, {type}, {data}')
|
|
return True
|
|
|
|
def __thread_check(self) -> None:
|
|
if threading.current_thread() is not self._mips_thread:
|
|
raise MIoTMipsError('illegal call')
|
|
|
|
def __mips_cmd_read_handler(self, ctx: any) -> None:
|
|
fd_value = os.eventfd_read(self._cmd_event_fd)
|
|
if fd_value == 0:
|
|
return
|
|
while self._mips_queue.empty() is False:
|
|
mips_cmd: MipsCmd = self._mips_queue.get(block=False)
|
|
if mips_cmd.type_ == MipsCmdType.CONNECT:
|
|
self._mips_reconnect_tag = True
|
|
self.__mips_try_reconnect(immediately=True)
|
|
elif mips_cmd.type_ == MipsCmdType.DISCONNECT:
|
|
self._mips_reconnect_tag = False
|
|
self.__mips_disconnect()
|
|
elif mips_cmd.type_ == MipsCmdType.DEINIT:
|
|
self.log_info('mips client recv deinit cmd')
|
|
self.__mips_disconnect()
|
|
# Close cmd event fd
|
|
if self._cmd_event_fd:
|
|
self.mev_set_read_handler(
|
|
self._cmd_event_fd, None, None)
|
|
os.close(self._cmd_event_fd)
|
|
self._cmd_event_fd = None
|
|
if self._mips_queue:
|
|
self._mips_queue = None
|
|
# ev loop stop
|
|
if self._mev:
|
|
self._mev.loop_stop()
|
|
self._mev = None
|
|
break
|
|
elif mips_cmd.type_ == MipsCmdType.REG_MIPS_STATE:
|
|
state: MipsState = mips_cmd.data
|
|
self._mips_state_sub_map[state.key] = state
|
|
self.log_debug(f'mips register mips state, {state.key}')
|
|
elif mips_cmd.type_ == MipsCmdType.UNREG_MIPS_STATE:
|
|
state: MipsState = mips_cmd.data
|
|
del self._mips_state_sub_map[state.key]
|
|
self.log_debug(f'mips unregister mips state, {state.key}')
|
|
else:
|
|
if self._on_mips_cmd:
|
|
self._on_mips_cmd(mips_cmd=mips_cmd)
|
|
|
|
def __mqtt_read_handler(self, ctx: any) -> None:
|
|
self.__mqtt_loop_handler(ctx=ctx)
|
|
|
|
def __mqtt_write_handler(self, ctx: any) -> None:
|
|
self.mev_set_write_handler(self._mqtt_fd, None, None)
|
|
self.__mqtt_loop_handler(ctx=ctx)
|
|
|
|
def __mqtt_timer_handler(self, ctx: any) -> None:
|
|
self.__mqtt_loop_handler(ctx=ctx)
|
|
if self._mqtt:
|
|
self._mqtt_timer = self.mev_set_timeout(
|
|
self.MQTT_INTERVAL_MS, self.__mqtt_timer_handler, None)
|
|
|
|
def __mqtt_loop_handler(self, ctx: any) -> None:
|
|
try:
|
|
if self._mqtt:
|
|
self._mqtt.loop_read()
|
|
if self._mqtt:
|
|
self._mqtt.loop_write()
|
|
if self._mqtt:
|
|
self._mqtt.loop_misc()
|
|
if self._mqtt and self._mqtt.want_write():
|
|
self.mev_set_write_handler(
|
|
self._mqtt_fd, self.__mqtt_write_handler, None)
|
|
except Exception as err: # pylint: disable=broad-exception-caught
|
|
# Catch all exception
|
|
self.log_error(f'__mqtt_loop_handler, {err}')
|
|
raise err
|
|
|
|
def __mips_loop_thread(self) -> None:
|
|
self.log_info('mips_loop_thread start')
|
|
# Set mqtt config
|
|
if self._username:
|
|
self._mqtt.username_pw_set(
|
|
username=self._username, password=self._password)
|
|
if (
|
|
self._ca_file
|
|
and self._cert_file
|
|
and self._key_file
|
|
):
|
|
self._mqtt.tls_set(
|
|
tls_version=ssl.PROTOCOL_TLS_CLIENT,
|
|
ca_certs=self._ca_file,
|
|
certfile=self._cert_file,
|
|
keyfile=self._key_file)
|
|
else:
|
|
self._mqtt.tls_set(tls_version=ssl.PROTOCOL_TLS_CLIENT)
|
|
self._mqtt.tls_insecure_set(True)
|
|
self._mqtt.on_connect = self.__on_connect
|
|
self._mqtt.on_connect_fail = self.__on_connect_failed
|
|
self._mqtt.on_disconnect = self.__on_disconnect
|
|
self._mqtt.on_message = self.__on_message
|
|
# Run event loop
|
|
self._mev.loop_forever()
|
|
self.log_info('mips_loop_thread exit!')
|
|
|
|
def __on_connect(self, client, user_data, flags, rc, props) -> None:
|
|
if not self._mqtt.is_connected():
|
|
return
|
|
self.log_info(f'mips connect, {flags}, {rc}, {props}')
|
|
self._mqtt_state = True
|
|
if self._on_mips_connect:
|
|
self.mev_set_timeout(
|
|
timeout_ms=0,
|
|
handler=lambda ctx:
|
|
self._on_mips_connect(rc, props))
|
|
for item in self._mips_state_sub_map.values():
|
|
if item.handler is None:
|
|
continue
|
|
self.main_loop.call_soon_threadsafe(
|
|
self.main_loop.create_task,
|
|
item.handler(item.key, True))
|
|
# Resolve future
|
|
self._event_connect.set()
|
|
self._event_disconnect.clear()
|
|
|
|
def __on_connect_failed(self, client, user_data, flags, rc) -> None:
|
|
self.log_error(f'mips connect failed, {flags}, {rc}')
|
|
# Try to reconnect
|
|
self.__mips_try_reconnect()
|
|
|
|
def __on_disconnect(self, client, user_data, rc, props) -> None:
|
|
if self._mqtt_state:
|
|
self.log_error(f'mips disconnect, {rc}, {props}')
|
|
self._mqtt_state = False
|
|
if self._mqtt_timer:
|
|
self.mev_clear_timeout(self._mqtt_timer)
|
|
self._mqtt_timer = None
|
|
if self._mqtt_fd != -1:
|
|
self.mev_set_read_handler(self._mqtt_fd, None, None)
|
|
self.mev_set_write_handler(self._mqtt_fd, None, None)
|
|
self._mqtt_fd = -1
|
|
# Clear retry sub
|
|
if self._mips_sub_pending_timer:
|
|
self.mev_clear_timeout(self._mips_sub_pending_timer)
|
|
self._mips_sub_pending_timer = None
|
|
self._mips_sub_pending_map = {}
|
|
if self._on_mips_disconnect:
|
|
self.mev_set_timeout(
|
|
timeout_ms=0,
|
|
handler=lambda ctx:
|
|
self._on_mips_disconnect(rc, props))
|
|
# Call state sub handler
|
|
for item in self._mips_state_sub_map.values():
|
|
if item.handler is None:
|
|
continue
|
|
self.main_loop.call_soon_threadsafe(
|
|
self.main_loop.create_task,
|
|
item.handler(item.key, False))
|
|
|
|
# Try to reconnect
|
|
self.__mips_try_reconnect()
|
|
# Set event
|
|
self._event_disconnect.set()
|
|
self._event_connect.clear()
|
|
|
|
def __on_message(self, client, user_data, msg) -> None:
|
|
self._on_mips_message(topic=msg.topic, payload=msg.payload)
|
|
|
|
def __mips_try_reconnect(self, immediately: bool = False) -> None:
|
|
if self._mips_reconnect_timer:
|
|
self.mev_clear_timeout(self._mips_reconnect_timer)
|
|
self._mips_reconnect_timer = None
|
|
if not self._mips_reconnect_tag:
|
|
return
|
|
interval: int = 0
|
|
if not immediately:
|
|
interval = self.__get_next_reconnect_time()
|
|
self.log_error(
|
|
'mips try reconnect after %sms', interval)
|
|
self._mips_reconnect_timer = self.mev_set_timeout(
|
|
interval, self.__mips_connect, None)
|
|
|
|
def __mips_sub_internal_pending_handler(self, ctx: any) -> None:
|
|
subbed_count = 1
|
|
for topic in list(self._mips_sub_pending_map.keys()):
|
|
if subbed_count > self.MIPS_SUB_PATCH:
|
|
break
|
|
count = self._mips_sub_pending_map[topic]
|
|
if count > 3:
|
|
self._mips_sub_pending_map.pop(topic)
|
|
self.log_error(f'retry mips sub internal error, {topic}')
|
|
continue
|
|
subbed_count += 1
|
|
result, mid = self._mqtt.subscribe(topic, qos=self.MIPS_QOS)
|
|
if result == MQTT_ERR_SUCCESS:
|
|
self._mips_sub_pending_map.pop(topic)
|
|
self.log_debug(f'mips sub internal success, {topic}')
|
|
continue
|
|
self._mips_sub_pending_map[topic] = count+1
|
|
self.log_error(
|
|
f'retry mips sub internal, {count}, {topic}, {result}, {mid}')
|
|
|
|
if len(self._mips_sub_pending_map):
|
|
self._mips_sub_pending_timer = self.mev_set_timeout(
|
|
self.MIPS_SUB_INTERVAL,
|
|
self.__mips_sub_internal_pending_handler, None)
|
|
else:
|
|
self._mips_sub_pending_timer = None
|
|
|
|
def __mips_connect(self, ctx: any = None) -> None:
|
|
result = MQTT_ERR_UNKNOWN
|
|
if self._mips_reconnect_timer:
|
|
self.mev_clear_timeout(self._mips_reconnect_timer)
|
|
self._mips_reconnect_timer = None
|
|
try:
|
|
# Try clean mqtt fd before mqtt connect
|
|
if self._mqtt_timer:
|
|
self.mev_clear_timeout(self._mqtt_timer)
|
|
self._mqtt_timer = None
|
|
if self._mqtt_fd != -1:
|
|
self.mev_set_read_handler(self._mqtt_fd, None, None)
|
|
self.mev_set_write_handler(self._mqtt_fd, None, None)
|
|
self._mqtt_fd = -1
|
|
result = self._mqtt.connect(
|
|
host=self._host, port=self._port,
|
|
clean_start=True, keepalive=MIHOME_MQTT_KEEPALIVE)
|
|
self.log_info(f'__mips_connect success, {result}')
|
|
except (TimeoutError, OSError) as error:
|
|
self.log_error('__mips_connect, connect error, %s', error)
|
|
|
|
if result == MQTT_ERR_SUCCESS:
|
|
self._mqtt_fd = self._mqtt.socket()
|
|
self.log_debug(f'__mips_connect, _mqtt_fd, {self._mqtt_fd}')
|
|
self.mev_set_read_handler(
|
|
self._mqtt_fd, self.__mqtt_read_handler, None)
|
|
if self._mqtt.want_write():
|
|
self.mev_set_write_handler(
|
|
self._mqtt_fd, self.__mqtt_write_handler, None)
|
|
self._mqtt_timer = self.mev_set_timeout(
|
|
self.MQTT_INTERVAL_MS, self.__mqtt_timer_handler, None)
|
|
else:
|
|
self.log_error(f'__mips_connect error result, {result}')
|
|
self.__mips_try_reconnect()
|
|
|
|
def __mips_disconnect(self) -> None:
|
|
if self._mips_reconnect_timer:
|
|
self.mev_clear_timeout(self._mips_reconnect_timer)
|
|
self._mips_reconnect_timer = None
|
|
if self._mqtt_timer:
|
|
self.mev_clear_timeout(self._mqtt_timer)
|
|
self._mqtt_timer = None
|
|
if self._mqtt_fd != -1:
|
|
self.mev_set_read_handler(self._mqtt_fd, None, None)
|
|
self.mev_set_write_handler(self._mqtt_fd, None, None)
|
|
self._mqtt_fd = -1
|
|
self._mqtt.disconnect()
|
|
|
|
def __get_next_reconnect_time(self) -> int:
|
|
if self._mips_reconnect_interval == 0:
|
|
self._mips_reconnect_interval = self.MIPS_RECONNECT_INTERVAL_MIN
|
|
else:
|
|
self._mips_reconnect_interval = min(
|
|
self._mips_reconnect_interval*2,
|
|
self.MIPS_RECONNECT_INTERVAL_MAX)
|
|
return self._mips_reconnect_interval
|
|
|
|
|
|
class MipsCloudClient(MipsClient):
|
|
"""MIoT Pub/Sub Cloud Client."""
|
|
# pylint: disable=unused-argument
|
|
_msg_matcher: MIoTMatcher
|
|
|
|
def __init__(
|
|
self, uuid: str, cloud_server: str, app_id: str,
|
|
token: str, port: int = 8883,
|
|
loop: Optional[asyncio.AbstractEventLoop] = None
|
|
) -> None:
|
|
self._msg_matcher = MIoTMatcher()
|
|
super().__init__(
|
|
client_id=f'ha.{uuid}', host=f'{cloud_server}-ha.mqtt.io.mi.com',
|
|
port=port, username=app_id, password=token, loop=loop)
|
|
|
|
self.on_mips_cmd = self.__on_mips_cmd_handler
|
|
self.on_mips_message = self.__on_mips_message_handler
|
|
self.on_mips_connect = self.__on_mips_connect_handler
|
|
self.on_mips_disconnect = self.__on_mips_disconnect_handler
|
|
|
|
def deinit(self) -> None:
|
|
self.mips_deinit()
|
|
self._msg_matcher = None
|
|
self.on_mips_cmd = None
|
|
self.on_mips_message = None
|
|
self.on_mips_connect = None
|
|
|
|
@final
|
|
def connect(self) -> None:
|
|
self.mips_connect()
|
|
|
|
@final
|
|
async def connect_async(self) -> None:
|
|
await self.mips_connect_async()
|
|
|
|
@final
|
|
def disconnect(self) -> None:
|
|
self.mips_disconnect()
|
|
self._msg_matcher = MIoTMatcher()
|
|
|
|
@final
|
|
async def disconnect_async(self) -> None:
|
|
await self.mips_disconnect_async()
|
|
self._msg_matcher = MIoTMatcher()
|
|
|
|
def update_access_token(self, access_token: str) -> bool:
|
|
if not isinstance(access_token, str):
|
|
raise MIoTMipsError('invalid token')
|
|
return self.update_mqtt_password(password=access_token)
|
|
|
|
@final
|
|
def sub_prop(
|
|
self, did: str, handler: Callable[[dict, any], None],
|
|
siid: int = None, piid: int = None, handler_ctx: any = None
|
|
) -> bool:
|
|
if not isinstance(did, str) or handler is None:
|
|
raise MIoTMipsError('invalid params')
|
|
|
|
topic: str = (
|
|
f'device/{did}/up/properties_changed/'
|
|
f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}')
|
|
|
|
def on_prop_msg(topic: str, payload: str, ctx: any) -> bool:
|
|
try:
|
|
msg: dict = json.loads(payload)
|
|
except json.JSONDecodeError:
|
|
self.log_error(
|
|
f'on_prop_msg, invalid msg, {topic}, {payload}')
|
|
return
|
|
if (
|
|
not isinstance(msg.get('params', None), dict)
|
|
or 'siid' not in msg['params']
|
|
or 'piid' not in msg['params']
|
|
or 'value' not in msg['params']
|
|
):
|
|
self.log_error(
|
|
f'on_prop_msg, invalid msg, {topic}, {payload}')
|
|
return
|
|
if handler:
|
|
self.log_debug('on properties_changed, %s', payload)
|
|
handler(msg['params'], ctx)
|
|
return self.__reg_broadcast(
|
|
topic=topic, handler=on_prop_msg, handler_ctx=handler_ctx)
|
|
|
|
@final
|
|
def unsub_prop(self, did: str, siid: int = None, piid: int = None) -> bool:
|
|
if not isinstance(did, str):
|
|
raise MIoTMipsError('invalid params')
|
|
topic: str = (
|
|
f'device/{did}/up/properties_changed/'
|
|
f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}')
|
|
return self.__unreg_broadcast(topic=topic)
|
|
|
|
@final
|
|
def sub_event(
|
|
self, did: str, handler: Callable[[dict, any], None],
|
|
siid: int = None, eiid: int = None, handler_ctx: any = None
|
|
) -> bool:
|
|
if not isinstance(did, str) or handler is None:
|
|
raise MIoTMipsError('invalid params')
|
|
# Spelling error: event_occured
|
|
topic: str = (
|
|
f'device/{did}/up/event_occured/'
|
|
f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}')
|
|
|
|
def on_event_msg(topic: str, payload: str, ctx: any) -> bool:
|
|
try:
|
|
msg: dict = json.loads(payload)
|
|
except json.JSONDecodeError:
|
|
self.log_error(
|
|
f'on_event_msg, invalid msg, {topic}, {payload}')
|
|
return
|
|
if (
|
|
not isinstance(msg.get('params', None), dict)
|
|
or 'siid' not in msg['params']
|
|
or 'eiid' not in msg['params']
|
|
or 'arguments' not in msg['params']
|
|
):
|
|
self.log_error(
|
|
f'on_event_msg, invalid msg, {topic}, {payload}')
|
|
return
|
|
if handler:
|
|
self.log_debug('on on_event_msg, %s', payload)
|
|
msg['params']['from'] = 'cloud'
|
|
handler(msg['params'], ctx)
|
|
return self.__reg_broadcast(
|
|
topic=topic, handler=on_event_msg, handler_ctx=handler_ctx)
|
|
|
|
@final
|
|
def unsub_event(self, did: str, siid: int = None, eiid: int = None) -> bool:
|
|
if not isinstance(did, str):
|
|
raise MIoTMipsError('invalid params')
|
|
# Spelling error: event_occured
|
|
topic: str = (
|
|
f'device/{did}/up/event_occured/'
|
|
f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}')
|
|
return self.__unreg_broadcast(topic=topic)
|
|
|
|
@final
|
|
def sub_device_state(
|
|
self, did: str, handler: Callable[[str, MIoTDeviceState, any], None],
|
|
handler_ctx: any = None
|
|
) -> bool:
|
|
"""subscribe online state."""
|
|
if not isinstance(did, str) or handler is None:
|
|
raise MIoTMipsError('invalid params')
|
|
topic: str = f'device/{did}/state/#'
|
|
|
|
def on_state_msg(topic: str, payload: str, ctx: any) -> None:
|
|
msg: dict = json.loads(payload)
|
|
# {"device_id":"xxxx","device_name":"米家智能插座3 ","event":"online",
|
|
# "model": "cuco.plug.v3","timestamp":1709001070828,"uid":xxxx}
|
|
if msg is None or 'device_id' not in msg or 'event' not in msg:
|
|
self.log_error(f'on_state_msg, recv unknown msg, {payload}')
|
|
return
|
|
if msg['device_id'] != did:
|
|
self.log_error(
|
|
f'on_state_msg, err msg, {did}!={msg["device_id"]}')
|
|
return
|
|
if handler:
|
|
self.log_debug('cloud, device state changed, %s', payload)
|
|
handler(
|
|
did, MIoTDeviceState.ONLINE if msg['event'] == 'online'
|
|
else MIoTDeviceState.OFFLINE, ctx)
|
|
return self.__reg_broadcast(
|
|
topic=topic, handler=on_state_msg, handler_ctx=handler_ctx)
|
|
|
|
@final
|
|
def unsub_device_state(self, did: str) -> bool:
|
|
if not isinstance(did, str):
|
|
raise MIoTMipsError('invalid params')
|
|
topic: str = f'device/{did}/state/#'
|
|
return self.__unreg_broadcast(topic=topic)
|
|
|
|
async def get_dev_list_async(
|
|
self, payload: str = None, timeout_ms: int = 10000
|
|
) -> dict[str, dict]:
|
|
raise NotImplementedError('please call in http client')
|
|
|
|
async def get_prop_async(
|
|
self, did: str, siid: int, piid: int, timeout_ms: int = 10000
|
|
) -> any:
|
|
raise NotImplementedError('please call in http client')
|
|
|
|
async def set_prop_async(
|
|
self, did: str, siid: int, piid: int, value: any,
|
|
timeout_ms: int = 10000
|
|
) -> bool:
|
|
raise NotImplementedError('please call in http client')
|
|
|
|
async def action_async(
|
|
self, did: str, siid: int, aiid: int, in_list: list,
|
|
timeout_ms: int = 10000
|
|
) -> tuple[bool, list]:
|
|
raise NotImplementedError('please call in http client')
|
|
|
|
def __on_mips_cmd_handler(self, mips_cmd: MipsCmd) -> None:
|
|
"""
|
|
NOTICE thread safe, this function will be called at the **mips** thread
|
|
"""
|
|
if mips_cmd.type_ == MipsCmdType.REG_BROADCAST:
|
|
reg_bc: MipsRegBroadcast = mips_cmd.data
|
|
if not self._msg_matcher.get(topic=reg_bc.topic):
|
|
sub_bc: MipsBroadcast = MipsBroadcast(
|
|
topic=reg_bc.topic, handler=reg_bc.handler,
|
|
handler_ctx=reg_bc.handler_ctx)
|
|
self._msg_matcher[reg_bc.topic] = sub_bc
|
|
self._mips_sub_internal(topic=reg_bc.topic)
|
|
else:
|
|
self.log_debug(f'mips cloud re-reg broadcast, {reg_bc.topic}')
|
|
elif mips_cmd.type_ == MipsCmdType.UNREG_BROADCAST:
|
|
unreg_bc: MipsRegBroadcast = mips_cmd.data
|
|
if self._msg_matcher.get(topic=unreg_bc.topic):
|
|
del self._msg_matcher[unreg_bc.topic]
|
|
self._mips_unsub_internal(topic=unreg_bc.topic)
|
|
|
|
def __reg_broadcast(
|
|
self, topic: str, handler: Callable[[str, str, any], None],
|
|
handler_ctx: any = None
|
|
) -> bool:
|
|
return self._mips_send_cmd(
|
|
type_=MipsCmdType.REG_BROADCAST,
|
|
data=MipsRegBroadcast(
|
|
topic=topic, handler=handler, handler_ctx=handler_ctx))
|
|
|
|
def __unreg_broadcast(self, topic: str) -> bool:
|
|
return self._mips_send_cmd(
|
|
type_=MipsCmdType.UNREG_BROADCAST,
|
|
data=MipsRegBroadcast(topic=topic))
|
|
|
|
def __on_mips_connect_handler(self, rc, props) -> None:
|
|
"""sub topic."""
|
|
for topic, _ in list(
|
|
self._msg_matcher.iter_all_nodes()):
|
|
self._mips_sub_internal(topic=topic)
|
|
|
|
def __on_mips_disconnect_handler(self, rc, props) -> None:
|
|
"""unsub topic."""
|
|
pass
|
|
|
|
def __on_mips_message_handler(self, topic: str, payload) -> None:
|
|
"""
|
|
NOTICE thread safe, this function will be called at the **mips** thread
|
|
"""
|
|
# broadcast
|
|
bc_list: list[MipsBroadcast] = list(
|
|
self._msg_matcher.iter_match(topic))
|
|
if not bc_list:
|
|
return
|
|
# self.log_debug(f"on broadcast, {topic}, {payload}")
|
|
for item in bc_list or []:
|
|
if item.handler is None:
|
|
continue
|
|
# NOTICE: call threadsafe
|
|
self.main_loop.call_soon_threadsafe(
|
|
item.handler, topic, payload, item.handler_ctx)
|
|
|
|
|
|
class MipsLocalClient(MipsClient):
|
|
"""MIoT Pub/Sub Local Client."""
|
|
# pylint: disable=unused-argument
|
|
MIPS_RECONNECT_INTERVAL_MIN: int = 6000
|
|
MIPS_RECONNECT_INTERVAL_MAX: int = 60000
|
|
MIPS_SUB_PATCH: int = 1000
|
|
MIPS_SUB_INTERVAL: int = 100
|
|
_did: str
|
|
_group_id: str
|
|
_home_name: str
|
|
_mips_seed_id: int
|
|
_reply_topic: str
|
|
_dev_list_change_topic: str
|
|
_request_map: dict[str, MipsRequest]
|
|
_msg_matcher: MIoTMatcher
|
|
_device_state_sub_map: dict[str, MipsDeviceState]
|
|
_get_prop_queue: dict[str, list]
|
|
_get_prop_timer: asyncio.TimerHandle
|
|
_on_dev_list_changed: Callable[[any, list[str]], asyncio.Future]
|
|
|
|
def __init__(
|
|
self, did: str, host: str, group_id: str,
|
|
ca_file: str, cert_file: str, key_file: str,
|
|
port: int = 8883, home_name: str = '',
|
|
loop: Optional[asyncio.AbstractEventLoop] = None
|
|
) -> None:
|
|
self._did = did
|
|
self._group_id = group_id
|
|
self._home_name = home_name
|
|
self._mips_seed_id = random.randint(0, self.UINT32_MAX)
|
|
self._reply_topic = f'{did}/reply'
|
|
self._dev_list_change_topic = f'{did}/appMsg/devListChange'
|
|
self._request_map = {}
|
|
self._msg_matcher = MIoTMatcher()
|
|
self._device_state_sub_map = {}
|
|
self._get_prop_queue = {}
|
|
self._get_prop_timer = None
|
|
self._on_dev_list_changed = None
|
|
|
|
super().__init__(
|
|
client_id=did, host=host, port=port,
|
|
ca_file=ca_file, cert_file=cert_file, key_file=key_file, loop=loop)
|
|
# MIPS local thread name use group_id
|
|
self._mips_thread.name = self._group_id
|
|
|
|
self.on_mips_cmd = self.__on_mips_cmd_handler
|
|
self.on_mips_message = self.__on_mips_message_handler
|
|
self.on_mips_connect = self.__on_mips_connect_handler
|
|
|
|
@property
|
|
def group_id(self) -> str:
|
|
return self._group_id
|
|
|
|
def deinit(self) -> None:
|
|
self.mips_deinit()
|
|
self._did = None
|
|
self._mips_seed_id = None
|
|
self._reply_topic = None
|
|
self._dev_list_change_topic = None
|
|
self._request_map = None
|
|
self._msg_matcher = None
|
|
self._device_state_sub_map = None
|
|
self._get_prop_queue = None
|
|
self._get_prop_timer = None
|
|
self._on_dev_list_changed = None
|
|
|
|
self.on_mips_cmd = None
|
|
self.on_mips_message = None
|
|
self.on_mips_connect = None
|
|
|
|
def log_debug(self, msg, *args, **kwargs) -> None:
|
|
if self._logger:
|
|
self._logger.debug(f'{self._home_name}, '+msg, *args, **kwargs)
|
|
|
|
def log_info(self, msg, *args, **kwargs) -> None:
|
|
if self._logger:
|
|
self._logger.info(f'{self._home_name}, '+msg, *args, **kwargs)
|
|
|
|
def log_error(self, msg, *args, **kwargs) -> None:
|
|
if self._logger:
|
|
self._logger.error(f'{self._home_name}, '+msg, *args, **kwargs)
|
|
|
|
@final
|
|
def connect(self) -> None:
|
|
self.mips_connect()
|
|
|
|
@final
|
|
async def connect_async(self) -> None:
|
|
await self.mips_connect_async()
|
|
|
|
@final
|
|
def disconnect(self) -> None:
|
|
self.mips_disconnect()
|
|
self._request_map = {}
|
|
self._msg_matcher = MIoTMatcher()
|
|
self._device_state_sub_map = {}
|
|
|
|
@final
|
|
async def disconnect_async(self) -> None:
|
|
await self.mips_disconnect_async()
|
|
self._request_map = {}
|
|
self._msg_matcher = MIoTMatcher()
|
|
self._device_state_sub_map = {}
|
|
|
|
@final
|
|
def sub_prop(
|
|
self, did: str, handler: Callable[[dict, any], None],
|
|
siid: int = None, piid: int = None, handler_ctx: any = None
|
|
) -> bool:
|
|
topic: str = (
|
|
f'appMsg/notify/iot/{did}/property/'
|
|
f'{"#" if siid is None or piid is None else f"{siid}.{piid}"}')
|
|
|
|
def on_prop_msg(topic: str, payload: str, ctx: any):
|
|
msg: dict = json.loads(payload)
|
|
if (
|
|
msg is None
|
|
or 'did' not in msg
|
|
or 'siid' not in msg
|
|
or 'piid' not in msg
|
|
or 'value' not in msg
|
|
):
|
|
# self.log_error(f'on_prop_msg, recv unknown msg, {payload}')
|
|
return
|
|
if handler:
|
|
self.log_debug('local, on properties_changed, %s', payload)
|
|
handler(msg, ctx)
|
|
return self.__reg_broadcast(
|
|
topic=topic, handler=on_prop_msg, handler_ctx=handler_ctx)
|
|
|
|
@final
|
|
def unsub_prop(self, did: str, siid: int = None, piid: int = None) -> bool:
|
|
topic: str = (
|
|
f'appMsg/notify/iot/{did}/property/'
|
|
f'{"#" if siid is None or piid is None else f"{siid}.{piid}"}')
|
|
return self.__unreg_broadcast(topic=topic)
|
|
|
|
@final
|
|
def sub_event(
|
|
self, did: str, handler: Callable[[dict, any], None],
|
|
siid: int = None, eiid: int = None, handler_ctx: any = None
|
|
) -> bool:
|
|
topic: str = (
|
|
f'appMsg/notify/iot/{did}/event/'
|
|
f'{"#" if siid is None or eiid is None else f"{siid}.{eiid}"}')
|
|
|
|
def on_event_msg(topic: str, payload: str, ctx: any):
|
|
msg: dict = json.loads(payload)
|
|
if (
|
|
msg is None
|
|
or 'did' not in msg
|
|
or 'siid' not in msg
|
|
or 'eiid' not in msg
|
|
or 'arguments' not in msg
|
|
):
|
|
# self.log_error(f'on_event_msg, recv unknown msg, {payload}')
|
|
return
|
|
if handler:
|
|
self.log_debug('local, on event_occurred, %s', payload)
|
|
handler(msg, ctx)
|
|
return self.__reg_broadcast(
|
|
topic=topic, handler=on_event_msg, handler_ctx=handler_ctx)
|
|
|
|
@final
|
|
def unsub_event(self, did: str, siid: int = None, eiid: int = None) -> bool:
|
|
topic: str = (
|
|
f'appMsg/notify/iot/{did}/event/'
|
|
f'{"#" if siid is None or eiid is None else f"{siid}.{eiid}"}')
|
|
return self.__unreg_broadcast(topic=topic)
|
|
|
|
@final
|
|
async def get_prop_safe_async(
|
|
self, did: str, siid: int, piid: int, timeout_ms: int = 10000
|
|
) -> any:
|
|
self._get_prop_queue.setdefault(did, [])
|
|
fut: asyncio.Future = self.main_loop.create_future()
|
|
self._get_prop_queue[did].append({
|
|
'param': json.dumps({
|
|
'did': did,
|
|
'siid': siid,
|
|
'piid': piid
|
|
}),
|
|
'fut': fut,
|
|
'timeout_ms': timeout_ms
|
|
})
|
|
if self._get_prop_timer is None:
|
|
self._get_prop_timer = self.main_loop.create_task(
|
|
self.__get_prop_timer_handle())
|
|
return await fut
|
|
|
|
@final
|
|
async def get_prop_async(
|
|
self, did: str, siid: int, piid: int, timeout_ms: int = 10000
|
|
) -> any:
|
|
result_obj = await self.__request_async(
|
|
topic='proxy/get',
|
|
payload=json.dumps({
|
|
'did': did,
|
|
'siid': siid,
|
|
'piid': piid
|
|
}),
|
|
timeout_ms=timeout_ms)
|
|
if not isinstance(result_obj, dict) or 'value' not in result_obj:
|
|
return None
|
|
return result_obj['value']
|
|
|
|
@final
|
|
async def set_prop_async(
|
|
self, did: str, siid: int, piid: int, value: any,
|
|
timeout_ms: int = 10000
|
|
) -> dict:
|
|
payload_obj: dict = {
|
|
'did': did,
|
|
'rpc': {
|
|
'id': self.__gen_mips_id,
|
|
'method': 'set_properties',
|
|
'params': [{
|
|
'did': did,
|
|
'siid': siid,
|
|
'piid': piid,
|
|
'value': value
|
|
}]
|
|
}
|
|
}
|
|
result_obj = await self.__request_async(
|
|
topic='proxy/rpcReq',
|
|
payload=json.dumps(payload_obj),
|
|
timeout_ms=timeout_ms)
|
|
if result_obj:
|
|
if (
|
|
'result' in result_obj
|
|
and len(result_obj['result']) == 1
|
|
and 'did' in result_obj['result'][0]
|
|
and result_obj['result'][0]['did'] == did
|
|
and 'code' in result_obj['result'][0]
|
|
):
|
|
return result_obj['result'][0]
|
|
if 'error' in result_obj:
|
|
return result_obj['error']
|
|
return {
|
|
'code': MIoTErrorCode.CODE_INTERNAL_ERROR.value,
|
|
'message': 'Invalid result'}
|
|
|
|
@final
|
|
async def action_async(
|
|
self, did: str, siid: int, aiid: int, in_list: list,
|
|
timeout_ms: int = 10000
|
|
) -> dict:
|
|
payload_obj: dict = {
|
|
'did': did,
|
|
'rpc': {
|
|
'id': self.__gen_mips_id,
|
|
'method': 'action',
|
|
'params': {
|
|
'did': did,
|
|
'siid': siid,
|
|
'aiid': aiid,
|
|
'in': in_list
|
|
}
|
|
}
|
|
}
|
|
result_obj = await self.__request_async(
|
|
topic='proxy/rpcReq', payload=json.dumps(payload_obj),
|
|
timeout_ms=timeout_ms)
|
|
if result_obj:
|
|
if 'result' in result_obj and 'code' in result_obj['result']:
|
|
return result_obj['result']
|
|
if 'error' in result_obj:
|
|
return result_obj['error']
|
|
return {
|
|
'code': MIoTErrorCode.CODE_INTERNAL_ERROR.value,
|
|
'message': 'Invalid result'}
|
|
|
|
@final
|
|
async def get_dev_list_async(
|
|
self, payload: str = None, timeout_ms: int = 10000
|
|
) -> dict[str, dict]:
|
|
result_obj = await self.__request_async(
|
|
topic='proxy/getDevList', payload=payload or '{}',
|
|
timeout_ms=timeout_ms)
|
|
if not result_obj or 'devList' not in result_obj:
|
|
return None
|
|
device_list = {}
|
|
for did, info in result_obj['devList'].items():
|
|
name: str = info.get('name', None)
|
|
urn: str = info.get('urn', None)
|
|
model: str = info.get('model', None)
|
|
if name is None or urn is None or model is None:
|
|
self.log_error(f'invalid device info, {did}, {info}')
|
|
continue
|
|
device_list[did] = {
|
|
'did': did,
|
|
'name': name,
|
|
'urn': urn,
|
|
'model': model,
|
|
'online': info.get('online', False),
|
|
'icon': info.get('icon', None),
|
|
'fw_version': None,
|
|
'home_id': '',
|
|
'home_name': '',
|
|
'room_id': info.get('roomId', ''),
|
|
'room_name': info.get('roomName', ''),
|
|
'specv2_access': info.get('specV2Access', False),
|
|
'push_available': info.get('pushAvailable', False),
|
|
'manufacturer': model.split('.')[0],
|
|
}
|
|
return device_list
|
|
|
|
@final
|
|
async def get_action_group_list_async(
|
|
self, timeout_ms: int = 10000
|
|
) -> list[str]:
|
|
result_obj = await self.__request_async(
|
|
topic='proxy/getMijiaActionGroupList',
|
|
payload='{}',
|
|
timeout_ms=timeout_ms)
|
|
if not result_obj or 'result' not in result_obj:
|
|
return None
|
|
return result_obj['result']
|
|
|
|
@final
|
|
async def exec_action_group_list_async(
|
|
self, ag_id: str, timeout_ms: int = 10000
|
|
) -> dict:
|
|
result_obj = await self.__request_async(
|
|
topic='proxy/execMijiaActionGroup',
|
|
payload=f'{{"id":"{ag_id}"}}',
|
|
timeout_ms=timeout_ms)
|
|
if result_obj:
|
|
if 'result' in result_obj:
|
|
return result_obj['result']
|
|
if 'error' in result_obj:
|
|
return result_obj['error']
|
|
return {
|
|
'code': MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value,
|
|
'message': 'invalid result'}
|
|
|
|
@final
|
|
@property
|
|
def on_dev_list_changed(self) -> Callable[[any, list[str]], asyncio.Future]:
|
|
return self._on_dev_list_changed
|
|
|
|
@final
|
|
@on_dev_list_changed.setter
|
|
def on_dev_list_changed(
|
|
self, func: Callable[[any, list[str]], asyncio.Future]
|
|
) -> None:
|
|
"""run in main loop."""
|
|
self._on_dev_list_changed = func
|
|
|
|
@final
|
|
def __on_mips_cmd_handler(self, mips_cmd: MipsCmd) -> None:
|
|
if mips_cmd.type_ == MipsCmdType.CALL_API:
|
|
req_data: MipsRequestData = mips_cmd.data
|
|
req = MipsRequest()
|
|
req.mid = self.__gen_mips_id
|
|
req.on_reply = req_data.on_reply
|
|
req.on_reply_ctx = req_data.on_reply_ctx
|
|
pub_topic: str = f'master/{req_data.topic}'
|
|
result = self.__mips_publish(
|
|
topic=pub_topic, payload=req_data.payload, mid=req.mid,
|
|
ret_topic=self._reply_topic)
|
|
self.log_debug(
|
|
f'mips local call api, {result}, {req.mid}, {pub_topic}, '
|
|
f'{req_data.payload}')
|
|
|
|
def on_request_timeout(req: MipsRequest):
|
|
self.log_error(
|
|
f'on mips request timeout, {req.mid}, {pub_topic}'
|
|
f', {req_data.payload}')
|
|
self._request_map.pop(str(req.mid), None)
|
|
req.on_reply(
|
|
'{"error":{"code":-10006, "message":"timeout"}}',
|
|
req.on_reply_ctx)
|
|
req.timer = self.mev_set_timeout(
|
|
req_data.timeout_ms, on_request_timeout, req)
|
|
self._request_map[str(req.mid)] = req
|
|
elif mips_cmd.type_ == MipsCmdType.REG_BROADCAST:
|
|
reg_bc: MipsRegBroadcast = mips_cmd.data
|
|
sub_topic: str = f'{self._did}/{reg_bc.topic}'
|
|
if not self._msg_matcher.get(sub_topic):
|
|
sub_bc: MipsBroadcast = MipsBroadcast(
|
|
topic=sub_topic, handler=reg_bc.handler,
|
|
handler_ctx=reg_bc.handler_ctx)
|
|
self._msg_matcher[sub_topic] = sub_bc
|
|
self._mips_sub_internal(topic=f'master/{reg_bc.topic}')
|
|
else:
|
|
self.log_debug(f'mips re-reg broadcast, {sub_topic}')
|
|
elif mips_cmd.type_ == MipsCmdType.UNREG_BROADCAST:
|
|
unreg_bc: MipsRegBroadcast = mips_cmd.data
|
|
# Central hub gateway needs to add prefix
|
|
unsub_topic: str = f'{self._did}/{unreg_bc.topic}'
|
|
if self._msg_matcher.get(unsub_topic):
|
|
del self._msg_matcher[unsub_topic]
|
|
self._mips_unsub_internal(
|
|
topic=re.sub(f'^{self._did}', 'master', unsub_topic))
|
|
elif mips_cmd.type_ == MipsCmdType.REG_DEVICE_STATE:
|
|
reg_dev_state: MipsRegDeviceState = mips_cmd.data
|
|
self._device_state_sub_map[reg_dev_state.did] = reg_dev_state
|
|
self.log_debug(
|
|
f'mips local reg device state, {reg_dev_state.did}')
|
|
elif mips_cmd.type_ == MipsCmdType.UNREG_DEVICE_STATE:
|
|
unreg_dev_state: MipsRegDeviceState = mips_cmd.data
|
|
del self._device_state_sub_map[unreg_dev_state.did]
|
|
self.log_debug(
|
|
f'mips local unreg device state, {unreg_dev_state.did}')
|
|
else:
|
|
self.log_error(
|
|
f'mips local recv unknown cmd, {mips_cmd.type_}, '
|
|
f'{mips_cmd.data}')
|
|
|
|
def __on_mips_connect_handler(self, rc, props) -> None:
|
|
self.log_debug('__on_mips_connect_handler')
|
|
# Sub did/#, include reply topic
|
|
self._mips_sub_internal(f'{self._did}/#')
|
|
# Sub device list change
|
|
self._mips_sub_internal('master/appMsg/devListChange')
|
|
# Do not need to subscribe api topics, for they are covered by did/#
|
|
# Sub api topic.
|
|
# Sub broadcast topic
|
|
for topic, _ in list(self._msg_matcher.iter_all_nodes()):
|
|
self._mips_sub_internal(
|
|
topic=re.sub(f'^{self._did}', 'master', topic))
|
|
|
|
@final
|
|
def __on_mips_message_handler(self, topic: str, payload: bytes) -> None:
|
|
mips_msg: MipsMessage = MipsMessage.unpack(payload)
|
|
# self.log_debug(
|
|
# f"mips local client, on_message, {topic} -> {mips_msg}")
|
|
# Reply
|
|
if topic == self._reply_topic:
|
|
self.log_debug(f'on request reply, {mips_msg}')
|
|
req: MipsRequest = self._request_map.pop(str(mips_msg.mid), None)
|
|
if req:
|
|
# Cancel timer
|
|
self.mev_clear_timeout(req.timer)
|
|
if req.on_reply:
|
|
self.main_loop.call_soon_threadsafe(
|
|
req.on_reply, mips_msg.payload or '{}',
|
|
req.on_reply_ctx)
|
|
return
|
|
# Broadcast
|
|
bc_list: list[MipsBroadcast] = list(self._msg_matcher.iter_match(
|
|
topic=topic))
|
|
if bc_list:
|
|
self.log_debug(f'on broadcast, {topic}, {mips_msg}')
|
|
for item in bc_list or []:
|
|
if item.handler is None:
|
|
continue
|
|
self.main_loop.call_soon_threadsafe(
|
|
item.handler, topic[topic.find('/')+1:],
|
|
mips_msg.payload or '{}', item.handler_ctx)
|
|
return
|
|
# Device list change
|
|
if topic == self._dev_list_change_topic:
|
|
payload_obj: dict = json.loads(mips_msg.payload)
|
|
dev_list = payload_obj.get('devList', None)
|
|
if not isinstance(dev_list, list) or not dev_list:
|
|
_LOGGER.error(
|
|
'unknown devListChange msg, %s', mips_msg.payload)
|
|
return
|
|
if self._on_dev_list_changed:
|
|
self.main_loop.call_soon_threadsafe(
|
|
self.main_loop.create_task,
|
|
self._on_dev_list_changed(self, payload_obj['devList']))
|
|
return
|
|
|
|
self.log_debug(
|
|
f'mips local client, recv unknown msg, {topic} -> {mips_msg}')
|
|
|
|
@property
|
|
def __gen_mips_id(self) -> int:
|
|
mips_id: int = self._mips_seed_id
|
|
self._mips_seed_id = int((self._mips_seed_id+1) % self.UINT32_MAX)
|
|
return mips_id
|
|
|
|
def __mips_publish(
|
|
self, topic: str, payload: str | bytes, mid: int = None,
|
|
ret_topic: str = None, wait_for_publish: bool = False,
|
|
timeout_ms: int = 10000
|
|
) -> bool:
|
|
mips_msg: bytes = MipsMessage.pack(
|
|
mid=mid or self.__gen_mips_id, payload=payload,
|
|
msg_from='local', ret_topic=ret_topic)
|
|
return self._mips_publish_internal(
|
|
topic=topic.strip(), payload=mips_msg,
|
|
wait_for_publish=wait_for_publish, timeout_ms=timeout_ms)
|
|
|
|
def __request(
|
|
self, topic: str, payload: str,
|
|
on_reply: Callable[[str, any], None],
|
|
on_reply_ctx: any = None, timeout_ms: int = 10000
|
|
) -> bool:
|
|
if topic is None or payload is None or on_reply is None:
|
|
raise MIoTMipsError('invalid params')
|
|
req_data: MipsRequestData = MipsRequestData()
|
|
req_data.topic = topic
|
|
req_data.payload = payload
|
|
req_data.on_reply = on_reply
|
|
req_data.on_reply_ctx = on_reply_ctx
|
|
req_data.timeout_ms = timeout_ms
|
|
return self._mips_send_cmd(type_=MipsCmdType.CALL_API, data=req_data)
|
|
|
|
def __reg_broadcast(
|
|
self, topic: str, handler: Callable[[str, str, any], None],
|
|
handler_ctx: any
|
|
) -> bool:
|
|
return self._mips_send_cmd(
|
|
type_=MipsCmdType.REG_BROADCAST,
|
|
data=MipsRegBroadcast(
|
|
topic=topic, handler=handler, handler_ctx=handler_ctx))
|
|
|
|
def __unreg_broadcast(self, topic) -> bool:
|
|
return self._mips_send_cmd(
|
|
type_=MipsCmdType.UNREG_BROADCAST,
|
|
data=MipsRegBroadcast(topic=topic))
|
|
|
|
@final
|
|
async def __request_async(
|
|
self, topic: str, payload: str, timeout_ms: int = 10000
|
|
) -> dict:
|
|
fut_handler: asyncio.Future = self.main_loop.create_future()
|
|
|
|
def on_msg_reply(payload: str, ctx: any):
|
|
fut: asyncio.Future = ctx
|
|
if fut:
|
|
self.main_loop.call_soon_threadsafe(fut.set_result, payload)
|
|
if not self.__request(
|
|
topic=topic,
|
|
payload=payload,
|
|
on_reply=on_msg_reply,
|
|
on_reply_ctx=fut_handler,
|
|
timeout_ms=timeout_ms):
|
|
# Request error
|
|
fut_handler.set_result('internal request error')
|
|
|
|
result = await fut_handler
|
|
try:
|
|
return json.loads(result)
|
|
except json.JSONDecodeError:
|
|
return {
|
|
'code': MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value,
|
|
'message': f'Error: {result}'}
|
|
|
|
async def __get_prop_timer_handle(self) -> None:
|
|
for did in list(self._get_prop_queue.keys()):
|
|
item = self._get_prop_queue[did].pop()
|
|
_LOGGER.debug('get prop, %s, %s', did, item)
|
|
result_obj = await self.__request_async(
|
|
topic='proxy/get',
|
|
payload=item['param'],
|
|
timeout_ms=item['timeout_ms'])
|
|
if result_obj is None or 'value' not in result_obj:
|
|
item['fut'].set_result(None)
|
|
else:
|
|
item['fut'].set_result(result_obj['value'])
|
|
|
|
if not self._get_prop_queue[did]:
|
|
self._get_prop_queue.pop(did, None)
|
|
|
|
if self._get_prop_queue:
|
|
self._get_prop_timer = self.main_loop.call_later(
|
|
0.1, lambda: self.main_loop.create_task(
|
|
self.__get_prop_timer_handle()))
|
|
else:
|
|
self._get_prop_timer = None
|