This commit is contained in:
unknown
2023-09-09 00:14:41 +08:00
parent 34c8e06cd8
commit 10d127c940
17 changed files with 690 additions and 125 deletions

View File

@@ -1,18 +1,18 @@
from aiohttp import ClientSession
from secrets import token_hex, token_urlsafe
from .security import CloudSecurity
from threading import Lock
import datetime
import logging
import time
import json
_LOGGER = logging.getLogger(__name__)
import logging
from aiohttp import ClientSession
from secrets import token_hex, token_urlsafe
from threading import Lock
from .security import CloudSecurity
CLIENT_TYPE = 1 # Android
FORMAT = 2 # JSON
APP_KEY = "4675636b"
_LOGGER = logging.getLogger(__name__)
class MideaCloudBase:
LANGUAGE = "en_US"
@@ -76,7 +76,7 @@ class MideaCloudBase:
response = json.loads(raw)
break
except Exception as e:
_LOGGER.debug(f"Cloud error: {repr(e)}")
_LOGGER.error(f"Cloud error: {repr(e)}")
if int(response["code"]) == 0 and "data" in response:
return response["data"]
return None
@@ -127,7 +127,7 @@ class MideaCloudBase:
udpid = CloudSecurity.get_udpid(device_id.to_bytes(6, "big"))
else:
udpid = CloudSecurity.get_udpid(device_id.to_bytes(6, "little"))
_LOGGER.debug(f"The udpid of deivce [{device_id}] generated "
_LOGGER.error(f"The udpid of deivce [{device_id}] generated "
f"with byte order '{'big' if byte_order_big else 'little'}': {udpid}")
response = await self.api_request(
"/v1/iot/secure/getToken",

View File

@@ -1,14 +1,11 @@
import threading
import socket
import time
from enum import IntEnum
from .security import LocalSecurity, MSGTYPE_HANDSHAKE_REQUEST, MSGTYPE_ENCRYPTED_REQUEST
from .packet_builder import PacketBuilder
from .lua_runtime import MideaCodec
import socket
import logging
import json
import time
_LOGGER = logging.getLogger(__name__)
from .logger import MideaLogger
class AuthException(Exception):
@@ -40,6 +37,7 @@ class MiedaDevice(threading.Thread):
key: str | None,
protocol: int,
model: str | None,
subtype: int | None,
sn: str | None,
sn8: str | None,
lua_file: str | None):
@@ -57,17 +55,21 @@ class MiedaDevice(threading.Thread):
self._protocol = protocol
self._model = model
self._updates = []
self._unsupported_protocol = []
self._is_run = False
self._device_protocol_version = 0
self._sub_type = None
self._subtype = subtype
self._sn = sn
self._sn8 = sn8
self._attributes = {}
self._attributes = {
"sn": sn,
"sn8": sn8,
"subtype": subtype
}
self._refresh_interval = 30
self._heartbeat_interval = 10
self._default_refresh_interval = 30
self._connected = False
self._lua_runtime = MideaCodec(lua_file, sn=sn) if lua_file is not None else None
self._queries = [{}]
self._centralized = []
self._lua_runtime = MideaCodec(lua_file, sn=sn, subtype=subtype) if lua_file is not None else None
@property
def device_name(self):
@@ -93,6 +95,10 @@ class MiedaDevice(threading.Thread):
def sn8(self):
return self._sn8
@property
def subtype(self):
return self._subtype
@property
def attributes(self):
return self._attributes
@@ -101,6 +107,50 @@ class MiedaDevice(threading.Thread):
def connected(self):
return self._connected
def set_refresh_interval(self, refresh_interval):
self._refresh_interval = refresh_interval
@property
def queries(self):
return self._queries
@queries.setter
def queries(self, queries: list):
self._queries = queries
@property
def centralized(self):
return self._centralized
@centralized.setter
def centralized(self, centralized: list):
self._centralized = centralized
def get_attribute(self, attribute):
return self._attributes.get(attribute)
def set_attribute(self, attribute, value):
if attribute in self._attributes.keys():
new_status = {}
for attr in self._centralized:
new_status[attr] = self._attributes.get(attr)
new_status[attribute] = value
set_cmd = self._lua_runtime.build_control(new_status)
self.build_send(set_cmd)
def set_attributes(self, attributes):
new_status = {}
for attr in self._centralized:
new_status[attr] = self._attributes.get(attr)
has_new = False
for attribute, value in attributes.items():
if attribute in self._attributes.keys():
has_new = True
new_status[attribute] = value
if has_new:
set_cmd = self._lua_runtime.build_control(new_status)
self.build_send(set_cmd)
@staticmethod
def fetch_v2_message(msg):
result = []
@@ -120,36 +170,36 @@ class MiedaDevice(threading.Thread):
try:
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._socket.settimeout(10)
_LOGGER.debug(f"[{self._device_id}] Connecting to {self._ip_address}:{self._port}")
MideaLogger.debug(f"Connecting to {self._ip_address}:{self._port}", self._device_id)
self._socket.connect((self._ip_address, self._port))
_LOGGER.debug(f"[{self._device_id}] Connected")
MideaLogger.debug(f"Connected", self._device_id)
if self._protocol == 3:
self.authenticate()
_LOGGER.debug(f"[{self._device_id}] Authentication success")
MideaLogger.debug(f"Authentication success", self._device_id)
self.device_connected(True)
if refresh:
self.refresh_status()
return True
except socket.timeout:
_LOGGER.debug(f"[{self._device_id}] Connection timed out")
MideaLogger.debug(f"Connection timed out", self._device_id)
except socket.error:
_LOGGER.debug(f"[{self._device_id}] Connection error")
MideaLogger.debug(f"Connection error", self._device_id)
except AuthException:
_LOGGER.debug(f"[{self._device_id}] Authentication failed")
MideaLogger.debug(f"Authentication failed", self._device_id)
except ResponseException:
_LOGGER.debug(f"[{self._device_id}] Unexpected response received")
MideaLogger.debug(f"Unexpected response received", self._device_id)
except RefreshFailed:
_LOGGER.debug(f"[{self._device_id}] Refresh status is timed out")
MideaLogger.debug(f"Refresh status is timed out", self._device_id)
except Exception as e:
_LOGGER.error(f"[{self._device_id}] Unknown error: {e.__traceback__.tb_frame.f_globals['__file__']}, "
f"{e.__traceback__.tb_lineno}, {repr(e)}")
MideaLogger.error(f"Unknown error: {e.__traceback__.tb_frame.f_globals['__file__']}, "
f"{e.__traceback__.tb_lineno}, {repr(e)}")
self.device_connected(False)
return False
def authenticate(self):
request = self._security.encode_8370(
self._token, MSGTYPE_HANDSHAKE_REQUEST)
_LOGGER.debug(f"[{self._device_id}] Handshaking")
MideaLogger.debug(f"Handshaking")
self._socket.send(request)
response = self._socket.recv(512)
if len(response) < 20:
@@ -167,21 +217,22 @@ class MiedaDevice(threading.Thread):
if self._socket is not None:
self._socket.send(data)
else:
_LOGGER.debug(f"[{self._device_id}] Send failure, device disconnected, data: {data.hex()}")
MideaLogger.debug(f"Send failure, device disconnected, data: {data.hex()}")
def send_message_v3(self, data, msg_type=MSGTYPE_ENCRYPTED_REQUEST):
data = self._security.encode_8370(data, msg_type)
self.send_message_v2(data)
def build_send(self, cmd):
_LOGGER.debug(f"[{self._device_id}] Sending: {cmd}")
MideaLogger.debug(f"Sending: {cmd}")
bytes_cmd = bytes.fromhex(cmd)
msg = PacketBuilder(self._device_id, bytes_cmd).finalize()
self.send_message(msg)
def refresh_status(self):
query_cmd = self._lua_runtime.build_query()
self.build_send(query_cmd)
for query in self._queries:
query_cmd = self._lua_runtime.build_query(query)
self.build_send(query_cmd)
def parse_message(self, msg):
if self._protocol == 3:
@@ -202,10 +253,10 @@ class MiedaDevice(threading.Thread):
cryptographic = message[40:-16]
if payload_len % 16 == 0:
decrypted = self._security.aes_decrypt(cryptographic)
_LOGGER.debug(f"[{self._device_id}] Received: {decrypted.hex()}")
MideaLogger.debug(f"Received: {decrypted.hex()}")
# 这就是最终消息
status = self._lua_runtime.decode_status(decrypted.hex())
_LOGGER.debug(f"[{self._device_id}] Decoded: {status}")
MideaLogger.debug(f"Decoded: {status}")
new_status = {}
for single in status.keys():
value = status.get(single)
@@ -229,7 +280,7 @@ class MiedaDevice(threading.Thread):
self._updates.append(update)
def update_all(self, status):
_LOGGER.debug(f"[{self._device_id}] Status update: {status}")
MideaLogger.debug(f"Status update: {status}")
for update in self._updates:
update(status)
@@ -241,17 +292,17 @@ class MiedaDevice(threading.Thread):
def close(self):
if self._is_run:
self._is_run = False
self._lua_runtime = None
self.close_socket()
def close_socket(self):
self._unsupported_protocol = []
self._buffer = b""
if self._socket:
self._socket.close()
self._socket = None
def set_ip_address(self, ip_address):
_LOGGER.debug(f"[{self._device_id}] Update IP address to {ip_address}")
MideaLogger.debug(f"Update IP address to {ip_address}")
self._ip_address = ip_address
self.close_socket()
@@ -283,7 +334,7 @@ class MiedaDevice(threading.Thread):
raise socket.error("Connection closed by peer")
result = self.parse_message(msg)
if result == ParseMessageResult.ERROR:
_LOGGER.debug(f"[{self._device_id}] Message 'ERROR' received")
MideaLogger.debug(f"Message 'ERROR' received")
self.close_socket()
break
elif result == ParseMessageResult.SUCCESS:
@@ -291,16 +342,16 @@ class MiedaDevice(threading.Thread):
except socket.timeout:
timeout_counter = timeout_counter + 1
if timeout_counter >= 120:
_LOGGER.debug(f"[{self._device_id}] Heartbeat timed out")
MideaLogger.debug(f"Heartbeat timed out")
self.close_socket()
break
except socket.error as e:
_LOGGER.debug(f"[{self._device_id}] Socket error {repr(e)}")
MideaLogger.debug(f"Socket error {repr(e)}")
self.close_socket()
break
except Exception as e:
_LOGGER.error(f"[{self._device_id}] Unknown error :{e.__traceback__.tb_frame.f_globals['__file__']}, "
f"{e.__traceback__.tb_lineno}, {repr(e)}")
MideaLogger.error(f"Unknown error :{e.__traceback__.tb_frame.f_globals['__file__']}, "
f"{e.__traceback__.tb_lineno}, {repr(e)}")
self.close_socket()
break

View File

@@ -1,14 +1,13 @@
import logging
import socket
import ifaddr
from ipaddress import IPv4Network
from .security import LocalSecurity
from .logger import MideaLogger
try:
import xml.etree.cElementTree as ET
except ImportError:
import xml.etree.ElementTree as ET
_LOGGER = logging.getLogger(__name__)
BROADCAST_MSG = bytearray([
0x5a, 0x5a, 0x01, 0x11, 0x48, 0x00, 0x92, 0x00,
@@ -34,7 +33,7 @@ DEVICE_INFO_MSG = bytearray([
def discover(discover_type=None, ip_address=None):
_LOGGER.debug(f"Begin discover, type: {discover_type}, ip_address: {ip_address}")
MideaLogger.debug(f"Begin discover, type: {discover_type}, ip_address: {ip_address}")
if discover_type is None:
discover_type = []
security = LocalSecurity()
@@ -55,7 +54,7 @@ def discover(discover_type=None, ip_address=None):
try:
data, addr = sock.recvfrom(512)
ip = addr[0]
_LOGGER.debug(f"Received broadcast from {addr}: {data.hex()}")
MideaLogger.debug(f"Received broadcast from {addr}: {data.hex()}")
if len(data) >= 104 and (data[:2].hex() == "5a5a" or data[8:10].hex() == "5a5a"):
if data[:2].hex() == "5a5a":
protocol = 2
@@ -70,7 +69,7 @@ def discover(discover_type=None, ip_address=None):
continue
encrypt_data = data[40:-16]
reply = security.aes_decrypt(encrypt_data)
_LOGGER.debug(f"Declassified reply: {reply.hex()}")
MideaLogger.debug(f"Declassified reply: {reply.hex()}")
ssid = reply[41:41 + reply[40]].decode("utf-8")
device_type = ssid.split("_")[1]
port = bytes2port(reply[4:8])
@@ -105,13 +104,13 @@ def discover(discover_type=None, ip_address=None):
}
if len(discover_type) == 0 or device.get("type") in discover_type:
found_devices[device_id] = device
_LOGGER.debug(f"Found a supported device: {device}")
MideaLogger.debug(f"Found a supported device: {device}")
else:
_LOGGER.debug(f"Found a unsupported device: {device}")
MideaLogger.debug(f"Found a unsupported device: {device}")
except socket.timeout:
break
except socket.error as e:
_LOGGER.debug(f"Socket error: {repr(e)}")
MideaLogger.debug(f"Socket error: {repr(e)}")
return found_devices
@@ -147,15 +146,15 @@ def get_device_info(device_ip, device_port: int):
sock.settimeout(8)
device_address = (device_ip, device_port)
sock.connect(device_address)
_LOGGER.debug(f"Sending to {device_ip}:{device_port} {DEVICE_INFO_MSG.hex()}")
MideaLogger.debug(f"Sending to {device_ip}:{device_port} {DEVICE_INFO_MSG.hex()}")
sock.sendall(DEVICE_INFO_MSG)
response = sock.recv(512)
except socket.timeout:
_LOGGER.warning(f"Connect the device {device_ip}:{device_port} timed out for 8s. "
MideaLogger.warning(f"Connect the device {device_ip}:{device_port} timed out for 8s. "
f"Don't care about a small amount of this. if many maybe not support."
)
except socket.error:
_LOGGER.warning(f"Can't connect to Device {device_ip}:{device_port}")
MideaLogger.warning(f"Can't connect to Device {device_ip}:{device_port}")
return response

View File

@@ -0,0 +1,36 @@
import inspect
import logging
from enum import IntEnum
class MideaLogType(IntEnum):
DEBUG = 1
WARN = 2
ERROR = 3
class MideaLogger:
@staticmethod
def _log(log_type, log, device_id):
frm = inspect.stack()[2]
mod = inspect.getmodule(frm[0])
if device_id is not None:
log = f"[{device_id}] {log}"
if log_type == MideaLogType.DEBUG:
logging.getLogger(mod.__name__).debug(log)
elif log_type == MideaLogType.WARN:
logging.getLogger(mod.__name__).warning(log)
elif log_type == MideaLogType.ERROR:
logging.getLogger(mod.__name__).error(log)
@staticmethod
def debug(log, device_id=None):
MideaLogger._log(MideaLogType.DEBUG, log, device_id)
@staticmethod
def warning(log, device_id=None):
MideaLogger._log(MideaLogType.WARN, log, device_id)
@staticmethod
def error(log, device_id=None):
MideaLogger._log(MideaLogType.ERROR, log, device_id)

View File

@@ -1,10 +1,7 @@
import lupa
import logging
import threading
import json
_LOGGER = logging.getLogger(__name__)
class LuaRuntime:
def __init__(self, file):
@@ -27,17 +24,17 @@ class LuaRuntime:
class MideaCodec(LuaRuntime):
def __init__(self, file, sn=None, sub_type=None):
def __init__(self, file, sn=None, subtype=None):
super().__init__(file)
self._sn = sn
self._sub_type = sub_type
self._subtype = subtype
def _build_base_dict(self):
device_info ={}
if self._sn is not None:
device_info["deviceSN"] = self._sn
if self._sub_type is not None:
device_info["deviceSubType"] = self._sub_type
if self._subtype is not None:
device_info["deviceSubType"] = self._subtype
base_dict = {
"deviceinfo": device_info
}

View File

@@ -6,7 +6,6 @@ class PacketBuilder:
def __init__(self, device_id: int, command):
self.command = None
self.security = LocalSecurity()
# aa20ac00000000000003418100ff03ff000200000000000000000000000006f274
# Init the packet with the header data.
self.packet = bytearray([
# 2 bytes - StaicHeader

View File

@@ -1,20 +1,18 @@
import hmac
import logging
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Util.strxor import strxor
from Crypto.Random import get_random_bytes
from hashlib import md5, sha256
from urllib.parse import urlparse
import hmac
import urllib
_LOGGER = logging.getLogger(__name__)
MSGTYPE_HANDSHAKE_REQUEST = 0x0
MSGTYPE_HANDSHAKE_RESPONSE = 0x1
MSGTYPE_ENCRYPTED_RESPONSE = 0x3
MSGTYPE_ENCRYPTED_REQUEST = 0x6
_LOGGER = logging.getLogger(__name__)
class CloudSecurity: