forked from HomeAssistant/midea-meiju-codec
v0.0.3
This commit is contained in:
@@ -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",
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
||||
|
36
custom_components/midea_meiju_codec/core/logger.py
Normal file
36
custom_components/midea_meiju_codec/core/logger.py
Normal 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)
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
||||
|
Reference in New Issue
Block a user