From 223b7148af9b8c96d65192fb9f788fa4f97d935d Mon Sep 17 00:00:00 2001 From: hodasemi Date: Fri, 22 Sep 2023 18:06:37 +0200 Subject: [PATCH] Remove pycache --- .gitignore | 5 +- __pycache__/discover.cpython-311.pyc | Bin 10681 -> 0 bytes __pycache__/security.cpython-311.pyc | Bin 17633 -> 0 bytes cloud.py | 316 ++++++++++++++++++++++ device.py | 390 +++++++++++++++++++++++++++ discover.py | 4 - message.py | 266 ++++++++++++++++++ midea.py | 51 +++- packet_builder.py | 60 +++++ src/cloud.rs | 7 + src/discover.rs | 11 +- src/lib.rs | 1 + src/security.rs | 2 +- 13 files changed, 1094 insertions(+), 19 deletions(-) delete mode 100644 __pycache__/discover.cpython-311.pyc delete mode 100644 __pycache__/security.cpython-311.pyc create mode 100644 cloud.py create mode 100644 device.py create mode 100644 message.py create mode 100644 packet_builder.py create mode 100644 src/cloud.rs diff --git a/.gitignore b/.gitignore index 869df07..ac0ef7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /target -Cargo.lock \ No newline at end of file +Cargo.lock + +*.pyc +__pycache__/ \ No newline at end of file diff --git a/__pycache__/discover.cpython-311.pyc b/__pycache__/discover.cpython-311.pyc deleted file mode 100644 index f32412ddb8a92152c793ed47a0e10c4abc129f93..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10681 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 diff --git a/cloud.py b/cloud.py new file mode 100644 index 0000000..0ddb5a4 --- /dev/null +++ b/cloud.py @@ -0,0 +1,316 @@ +import logging +import time +import datetime +import json +import base64 +from threading import Lock +from aiohttp import ClientSession +from secrets import token_hex +from security import CloudSecurity, MeijuCloudSecurity, MSmartCloudSecurity + +_LOGGER = logging.getLogger(__name__) + +clouds = { + "美的美居": { + "class_name": "MeijuCloud", + "app_id": "900", + "app_key": "46579c15", + "login_key": "ad0ee21d48a64bf49f4fb583ab76e799", + "iot_key": bytes.fromhex(format(9795516279659324117647275084689641883661667, 'x')).decode(), + "hmac_key": bytes.fromhex(format(117390035944627627450677220413733956185864939010425, 'x')).decode(), + "api_url": "https://mp-prod.smartmidea.net/mas/v5/app/proxy?alias=", + }, + "MSmartHome": { + "class_name": "MSmartHomeCloud", + "app_id": "1010", + "app_key": "ac21b9f9cbfe4ca5a88562ef25e2b768", + "iot_key": bytes.fromhex(format(7882822598523843940, 'x')).decode(), + "hmac_key": bytes.fromhex(format(117390035944627627450677220413733956185864939010425, 'x')).decode(), + "api_url": "https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=", + } +} + +default_keys = { + 99: { + "token": "ee755a84a115703768bcc7c6c13d3d629aa416f1e2fd798beb9f78cbb1381d09" + "1cc245d7b063aad2a900e5b498fbd936c811f5d504b2e656d4f33b3bbc6d1da3", + "key": "ed37bd31558a4b039aaf4e7a7a59aa7a75fd9101682045f69baf45d28380ae5c" + } +} + + +class MideaCloud: + def __init__( + self, + session: ClientSession, + security: CloudSecurity, + app_key: str, + account: str, + password: str, + api_url: str + ): + self._device_id = CloudSecurity.get_deviceid(account) + self._session = session + self._security = security + self._api_lock = Lock() + self._app_key = app_key + self._account = account + self._password = password + self._api_url = api_url + self._access_token = None + self._login_id = None + + def _make_general_data(self): + return {} + + async def _api_request(self, endpoint: str, data: dict, header=None) -> dict | None: + header = header or {} + if not data.get("reqId"): + data.update({ + "reqId": token_hex(16) + }) + if not data.get("stamp"): + data.update({ + "stamp": datetime.datetime.now().strftime("%Y%m%d%H%M%S") + }) + random = str(int(time.time())) + url = self._api_url + endpoint + dump_data = json.dumps(data) + sign = self._security.sign(dump_data, random) + header.update({ + "content-type": "application/json; charset=utf-8", + "secretVersion": "1", + "sign": sign, + "random": random, + }) + if self._access_token is not None: + header.update({ + "accesstoken": self._access_token + }) + response:dict = {"code": -1} + for i in range(0, 3): + try: + with self._api_lock: + r = await self._session.request("POST", url, headers=header, data=dump_data, timeout=10) + raw = await r.read() + _LOGGER.debug(f"Midea cloud API url: {url}, data: {data}, response: {raw}") + response = json.loads(raw) + break + except Exception as e: + pass + if int(response["code"]) == 0 and "data" in response: + return response["data"] + print(response) + return None + + async def _get_login_id(self) -> str | None: + data = self._make_general_data() + data.update({ + "loginAccount": f"{self._account}" + }) + if response := await self._api_request( + endpoint="/v1/user/login/id/get", + data=data + ): + return response.get("loginId") + return None + + async def login(self) -> bool: + raise NotImplementedError() + + async def get_keys(self, appliance_id: int): + result = {} + for method in [1, 2]: + udp_id = self._security.get_udp_id(appliance_id, method) + data = self._make_general_data() + data.update({ + "udpid": udp_id + }) + response = await self._api_request( + endpoint="/v1/iot/secure/getToken", + data=data + ) + if response and "tokenlist" in response: + for token in response["tokenlist"]: + if token["udpId"] == udp_id: + result[method] = { + "token": token["token"].lower(), + "key": token["key"].lower() + } + result.update(default_keys) + return result + + +class MeijuCloud(MideaCloud): + APP_ID = "900" + APP_VERSION = "8.20.0.2" + + def __init__( + self, + cloud_name: str, + session: ClientSession, + account: str, + password: str, + ): + super().__init__( + session=session, + security=MeijuCloudSecurity( + login_key=clouds[cloud_name].get("login_key"), + iot_key=clouds[cloud_name].get("iot_key"), + hmac_key=clouds[cloud_name].get("hmac_key"), + ), + app_key=clouds[cloud_name]["app_key"], + account=account, + password=password, + api_url=clouds[cloud_name]["api_url"] + ) + + async def login(self) -> bool: + if login_id := await self._get_login_id(): + self._login_id = login_id + stamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + data = { + "iotData": { + "clientType": 1, + "deviceId": self._device_id, + "iampwd": self._security.encrypt_iam_password(self._login_id, self._password), + "iotAppId": self.APP_ID, + "loginAccount": self._account, + "password": self._security.encrypt_password(self._login_id, self._password), + "reqId": token_hex(16), + "stamp": stamp + }, + "data": { + "appKey": self._app_key, + "deviceId": self._device_id, + "platform": 2 + }, + "timestamp": stamp, + "stamp": stamp + } + if response := await self._api_request( + endpoint="/mj/user/login", + data=data + ): + self._access_token = response["mdata"]["accessToken"] + self._security.set_aes_keys( + self._security.aes_decrypt_with_fixed_key( + response["key"] + ), None + ) + + return True + return False + + +class MSmartHomeCloud(MideaCloud): + APP_ID = "1010" + SRC = "10" + APP_VERSION = "3.0.2" + + def __init__( + self, + cloud_name: str, + session: ClientSession, + account: str, + password: str, + ): + super().__init__( + session=session, + security=MSmartCloudSecurity( + login_key=clouds[cloud_name].get("app_key"), + iot_key=clouds[cloud_name].get("iot_key"), + hmac_key=clouds[cloud_name].get("hmac_key"), + ), + app_key=clouds[cloud_name]["app_key"], + account=account, + password=password, + api_url=clouds[cloud_name]["api_url"] + ) + self._auth_base = base64.b64encode( + f"{self._app_key}:{clouds['MSmartHome']['iot_key']}".encode("ascii") + ).decode("ascii") + self._uid = "" + + def _make_general_data(self): + return { + "appVersion": self.APP_VERSION, + "src": self.SRC, + "format": "2", + "stamp": datetime.datetime.now().strftime("%Y%m%d%H%M%S"), + "platformId": "1", + "deviceId": self._device_id, + "reqId": token_hex(16), + "uid": self._uid, + "clientType": "1", + "appId": self.APP_ID, + } + + async def _api_request(self, endpoint: str, data: dict, header=None) -> dict | None: + header = header or {} + header.update({ + "x-recipe-app": self.APP_ID, + "authorization": f"Basic {self._auth_base}" + }) + if len(self._uid) > 0: + header.update({ + "uid": self._uid + }) + return await super()._api_request(endpoint, data, header) + + async def _re_route(self): + data = self._make_general_data() + data.update({ + "userType": "0", + "userName": f"{self._account}" + }) + if response := await self._api_request( + endpoint="/v1/multicloud/platform/user/route", + data=data + ): + if api_url := response.get("masUrl"): + self._api_url = api_url + + async def login(self) -> bool: + await self._re_route() + if login_id := await self._get_login_id(): + self._login_id = login_id + iot_data = self._make_general_data() + iot_data.pop("uid") + stamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + iot_data.update({ + "iampwd": self._security.encrypt_iam_password(self._login_id, self._password), + "loginAccount": self._account, + "password": self._security.encrypt_password(self._login_id, self._password), + "stamp": stamp + }) + data = { + "iotData": iot_data, + "data": { + "appKey": self._app_key, + "deviceId": self._device_id, + "platform": "2" + }, + "stamp": stamp + } + if response := await self._api_request( + endpoint="/mj/user/login", + data=data + ): + self._uid = response["uid"] + self._access_token = response["mdata"]["accessToken"] + self._security.set_aes_keys(response["accessToken"], response["randomData"]) + return True + return False + + +def get_midea_cloud(cloud_name: str, session: ClientSession, account: str, password: str) -> MideaCloud | None: + cloud = None + if cloud_name in clouds.keys(): + cloud = globals()[clouds[cloud_name]["class_name"]]( + cloud_name=cloud_name, + session=session, + account=account, + password=password + ) + return cloud diff --git a/device.py b/device.py new file mode 100644 index 0000000..b049bcb --- /dev/null +++ b/device.py @@ -0,0 +1,390 @@ +import threading +try: + from enum import StrEnum +except ImportError: + from ..backports.enum import StrEnum +from enum import IntEnum +from security import LocalSecurity, MSGTYPE_HANDSHAKE_REQUEST, MSGTYPE_ENCRYPTED_REQUEST +from packet_builder import PacketBuilder +from message import MessageType, MessageQuerySubtype, MessageSubtypeResponse, MessageQuestCustom +import socket +import logging +import time + +_LOGGER = logging.getLogger(__name__) + + +class AuthException(Exception): + pass + + +class ResponseException(Exception): + pass + + +class RefreshFailed(Exception): + pass + + +class DeviceAttributes(StrEnum): + pass + + +class ParseMessageResult(IntEnum): + SUCCESS = 0 + PADDING = 1 + ERROR = 99 + + +class MiedaDevice(threading.Thread): + def __init__(self, + name: str, + device_id: int, + device_type: int, + ip_address: str, + port: int, + token: str, + key: str, + protocol: int, + model: str, + attributes: dict): + threading.Thread.__init__(self) + self._attributes = attributes if attributes else {} + self._socket = None + self._ip_address = ip_address + self._port = port + self._security = LocalSecurity() + self._token = bytes.fromhex(token) if token else None + self._key = bytes.fromhex(key) if key else None + self._buffer = b"" + self._device_name = name + self._device_id = device_id + self._device_type = device_type + self._protocol = protocol + self._model = model + self._updates = [] + self._unsupported_protocol = [] + self._is_run = False + self._available = True + self._device_protocol_version = 0 + self._sub_type = None + self._sn = None + self._refresh_interval = 30 + self._heartbeat_interval = 10 + self._default_refresh_interval = 30 + + @property + def name(self): + return self._device_name + + @property + def available(self): + return self._available + + @property + def device_id(self): + return self._device_id + + @property + def device_type(self): + return self._device_type + + @property + def model(self): + return self._model + + @property + def sub_type(self): + return self._sub_type if self._sub_type else 0 + + @staticmethod + def fetch_v2_message(msg): + result = [] + while len(msg) > 0: + factual_msg_len = len(msg) + if factual_msg_len < 6: + break + alleged_msg_len = msg[4] + (msg[5] << 8) + if factual_msg_len >= alleged_msg_len: + result.append(msg[:alleged_msg_len]) + msg = msg[alleged_msg_len:] + else: + break + return result, msg + + def connect(self, refresh_status=True): + 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}") + self._socket.connect((self._ip_address, self._port)) + _LOGGER.debug(f"[{self._device_id}] Connected") + if self._protocol == 3: + self.authenticate() + _LOGGER.debug(f"[{self._device_id}] Authentication success") + if refresh_status: + self.refresh_status(wait_response=True) + self.enable_device(True) + return True + except socket.timeout: + _LOGGER.debug(f"[{self._device_id}] Connection timed out") + except socket.error: + _LOGGER.debug(f"[{self._device_id}] Connection error") + except AuthException: + _LOGGER.debug(f"[{self._device_id}] Authentication failed") + except ResponseException: + _LOGGER.debug(f"[{self._device_id}] Unexpected response received") + except RefreshFailed: + _LOGGER.debug(f"[{self._device_id}] Refresh status is timed out") + 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)}") + self.enable_device(False) + return False + + def authenticate(self): + request = self._security.encode_8370( + self._token, MSGTYPE_HANDSHAKE_REQUEST) + _LOGGER.debug(f"[{self._device_id}] Handshaking") + self._socket.send(request) + response = self._socket.recv(512) + if len(response) < 20: + raise AuthException() + response = response[8: 72] + self._security.tcp_key(response, self._key) + + def send_message(self, data): + if self._protocol == 3: + self.send_message_v3(data, msg_type=MSGTYPE_ENCRYPTED_REQUEST) + else: + self.send_message_v2(data) + + def send_message_v2(self, data): + if self._socket is not None: + self._socket.send(data) + else: + _LOGGER.debug(f"[{self._device_id}] 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): + data = cmd.serialize() + _LOGGER.debug(f"[{self._device_id}] Sending: {cmd}") + msg = PacketBuilder(self._device_id, data).finalize() + self.send_message(msg) + + def refresh_status(self, wait_response=False): + cmds = self.build_query() + if self._sub_type is None: + cmds = [MessageQuerySubtype(self.device_type)] + cmds + error_count = 0 + for cmd in cmds: + if cmd.__class__.__name__ not in self._unsupported_protocol: + self.build_send(cmd) + if wait_response: + try: + while True: + msg = self._socket.recv(512) + if len(msg) == 0: + raise socket.error + result = self.parse_message(msg) + if result == ParseMessageResult.SUCCESS: + break + elif result == ParseMessageResult.PADDING: + continue + else: + raise ResponseException + except socket.timeout: + error_count += 1 + self._unsupported_protocol.append(cmd.__class__.__name__) + _LOGGER.debug(f"[{self._device_id}] Does not supports " + f"the protocol {cmd.__class__.__name__}, ignored") + except ResponseException: + error_count += 1 + else: + error_count += 1 + if error_count == len(cmds): + raise RefreshFailed + + def set_subtype(self): + pass + + def pre_process_message(self, msg): + if msg[9] == MessageType.querySubtype: + message = MessageSubtypeResponse(msg) + _LOGGER.debug(f"[{self.device_id}] Received: {message}") + self._sub_type = message.sub_type + self.set_subtype() + self._device_protocol_version = message.device_protocol_version + _LOGGER.debug(f"[{self._device_id}] Subtype: {self._sub_type}. " + f"Device protocol version: {self._device_protocol_version}") + return False + return True + + def parse_message(self, msg): + if self._protocol == 3: + messages, self._buffer = self._security.decode_8370(self._buffer + msg) + else: + messages, self._buffer = self.fetch_v2_message(self._buffer + msg) + if len(messages) == 0: + return ParseMessageResult.PADDING + for message in messages: + if message == b"ERROR": + return ParseMessageResult.ERROR + payload_len = message[4] + (message[5] << 8) - 56 + payload_type = message[2] + (message[3] << 8) + if payload_type in [0x1001, 0x0001]: + # Heartbeat detected + pass + elif len(message) > 56: + cryptographic = message[40:-16] + if payload_len % 16 == 0: + decrypted = self._security.aes_decrypt(cryptographic) + if self.pre_process_message(decrypted): + try: + status = self.process_message(decrypted) + if len(status) > 0: + self.update_all(status) + else: + _LOGGER.debug(f"[{self._device_id}] Unidentified protocol") + except Exception as e: + _LOGGER.error(f"[{self._device_id}] Error in process message, msg = {decrypted.hex()}") + else: + _LOGGER.warning( + f"[{self._device_id}] Illegal payload, " + f"original message = {msg.hex()}, buffer = {self._buffer.hex()}, " + f"8370 decoded = {message.hex()}, payload type = {payload_type}, " + f"alleged payload length = {payload_len}, factual payload length = {len(cryptographic)}" + ) + else: + _LOGGER.warning( + f"[{self._device_id}] Illegal message, " + f"original message = {msg.hex()}, buffer = {self._buffer.hex()}, " + f"8370 decoded = {message.hex()}, payload type = {payload_type}, " + f"alleged payload length = {payload_len}, message length = {len(message)}, " + ) + return ParseMessageResult.SUCCESS + + def build_query(self): + raise NotImplementedError + + def process_message(self, msg): + raise NotImplementedError + + def send_command(self, cmd_type, cmd_body: bytearray): + cmd = MessageQuestCustom(self._device_type, cmd_type, cmd_body) + try: + self.build_send(cmd) + except socket.error as e: + _LOGGER.debug(f"[{self._device_id}] Interface send_command failure, {repr(e)}, " + f"cmd_type: {cmd_type}, cmd_body: {cmd_body.hex()}") + + def send_heartbeat(self): + msg = PacketBuilder(self._device_id, bytearray([0x00])).finalize(msg_type=0) + self.send_message(msg) + + def register_update(self, update): + self._updates.append(update) + + def update_all(self, status): + _LOGGER.debug(f"[{self._device_id}] Status update: {status}") + for update in self._updates: + update(status) + + def enable_device(self, available=True): + self._available = available + status = {"available": available} + self.update_all(status) + + def open(self): + if not self._is_run: + self._is_run = True + threading.Thread.start(self) + + def close(self): + if self._is_run: + self._is_run = False + 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): + if self._ip_address != ip_address: + _LOGGER.debug(f"[{self._device_id}] Update IP address to {ip_address}") + self._ip_address = ip_address + self.close_socket() + + def set_refresh_interval(self, refresh_interval): + self._refresh_interval = refresh_interval + + def run(self): + while self._is_run: + while self._socket is None: + if self.connect(refresh_status=True) is False: + if not self._is_run: + return + self.close_socket() + time.sleep(5) + timeout_counter = 0 + start = time.time() + previous_refresh = start + previous_heartbeat = start + self._socket.settimeout(1) + while True: + try: + now = time.time() + if 0 < self._refresh_interval <= now - previous_refresh: + self.refresh_status() + previous_refresh = now + if now - previous_heartbeat >= self._heartbeat_interval: + self.send_heartbeat() + previous_heartbeat = now + msg = self._socket.recv(512) + msg_len = len(msg) + if msg_len == 0: + 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") + self.close_socket() + break + elif result == ParseMessageResult.SUCCESS: + timeout_counter = 0 + except socket.timeout: + timeout_counter = timeout_counter + 1 + if timeout_counter >= 120: + _LOGGER.debug(f"[{self._device_id}] Heartbeat timed out") + self.close_socket() + break + except socket.error as e: + _LOGGER.debug(f"[{self._device_id}] 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)}") + self.close_socket() + break + + # def set_attribute(self, attr, value): + # raise NotImplementedError + + def get_attribute(self, attr): + return self._attributes.get(attr) + + def set_customize(self, customize): + pass + + @property + def attributes(self): + ret = {} + for status in self._attributes.keys(): + ret[str(status)] = self._attributes[status] + return ret diff --git a/discover.py b/discover.py index df604fb..edd295e 100644 --- a/discover.py +++ b/discover.py @@ -65,10 +65,6 @@ def discover(discover_type=None, ip_address=None): else: continue - print(data[20:26]) - print(data[20:26].hex()) - print(bytearray.fromhex(data[20:26].hex())) - device_id = int.from_bytes(bytearray.fromhex(data[20:26].hex()), "little") if device_id in found_devices: continue diff --git a/message.py b/message.py new file mode 100644 index 0000000..571ee01 --- /dev/null +++ b/message.py @@ -0,0 +1,266 @@ +import logging +from abc import ABC +from enum import IntEnum + +_LOGGER = logging.getLogger(__name__) + + +class MessageLenError(Exception): + pass + + +class MessageBodyError(Exception): + pass + + +class MessageCheckSumError(Exception): + pass + + +class MessageType(IntEnum): + set = 0x02, + query = 0x03, + notify1 = 0x04, + notify2 = 0x05, + exception = 0x06, + querySN = 0x07, + exception2 = 0x0A, + querySubtype = 0xA0 + + +class MessageBase(ABC): + HEADER_LENGTH = 10 + + def __init__(self): + self._device_type = 0x00 + self._message_type = 0x00 + self._body_type = 0x00 + self._device_protocol_version = 0 + + @staticmethod + def checksum(data): + return (~ sum(data) + 1) & 0xff + + # @property + # def header(self): + # raise NotImplementedError + + # @property + # def body(self): + # raise NotImplementedError + + @property + def message_type(self): + return self._message_type + + @message_type.setter + def message_type(self, value): + self._message_type = value + + @property + def device_type(self): + return self._device_type + + @device_type.setter + def device_type(self, value): + self._device_type = value + + @property + def body_type(self): + return self._body_type + + @body_type.setter + def body_type(self, value): + self._body_type = value + + @property + def device_protocol_version(self): + return self._device_protocol_version + + @device_protocol_version.setter + def device_protocol_version(self, value): + self._device_protocol_version = value + + def __str__(self) -> str: + output = { + "header": self.header.hex(), + "body": self.body.hex(), + "message type": "%02x" % self._message_type, + "body type": ("%02x" % self._body_type) if self._body_type is not None else "None" + } + return str(output) + + +class MessageRequest(MessageBase): + def __init__(self, device_protocol_version, device_type, message_type, body_type): + super().__init__() + self.device_protocol_version = device_protocol_version + self.device_type = device_type + self.message_type = message_type + self.body_type = body_type + + @property + def header(self): + length = self.HEADER_LENGTH + len(self.body) + return bytearray([ + # flag + 0xAA, + # length + length, + # device type + self._device_type, + # frame checksum + 0x00, # self._device_type ^ length, + # unused + 0x00, 0x00, + # frame ID + 0x00, + # frame protocol version + 0x00, + # device protocol version + self._device_protocol_version, + # frame type + self._message_type + ]) + + @property + def _body(self): + raise NotImplementedError + + @property + def body(self): + body = bytearray([]) + if self.body_type is not None: + body.append(self.body_type) + if self._body is not None: + body.extend(self._body) + return body + + def serialize(self): + stream = self.header + self.body + stream.append(MessageBase.checksum(stream[1:])) + return stream + + +class MessageQuerySubtype(MessageRequest): + def __init__(self, device_type): + super().__init__( + device_protocol_version=0, + device_type=device_type, + message_type=MessageType.querySubtype, + body_type=0x00) + + @property + def _body(self): + return bytearray([0x00] * 18) + + +class MessageQuestCustom(MessageRequest): + def __init__(self, device_type, cmd_type, cmd_body): + super().__init__( + device_protocol_version=0, + device_type=device_type, + message_type=cmd_type, + body_type=None) + self._cmd_body = cmd_body + + @property + def _body(self): + return bytearray([]) + + @property + def body(self): + return self._cmd_body + + +class MessageBody: + def __init__(self, body): + self._data = body + + @property + def data(self): + return self._data + + @property + def body_type(self): + return self._data[0] + + @staticmethod + def read_byte(body, byte, default_value=0): + return body[byte] if len(body) > byte else default_value + + +class NewProtocolMessageBody(MessageBody): + def __init__(self, body, bt): + super().__init__(body) + if bt == 0xb5: + self._pack_len = 4 + else: + self._pack_len = 5 + + @staticmethod + def pack(param, value: bytearray, pack_len=4): + length = len(value) + if pack_len == 4: + stream = bytearray([param & 0xFF, param >> 8, length]) + value + else: + stream = bytearray([param & 0xFF, param >> 8, 0x00, length]) + value + return stream + + def parse(self): + result = {} + try: + pos = 2 + for pack in range(0, self.data[1]): + param = self.data[pos] + (self.data[pos + 1] << 8) + if self._pack_len == 5: + pos += 1 + length = self.data[pos + 2] + if length > 0: + value = self.data[pos + 3: pos + 3 + length] + result[param] = value + pos += (3 + length) + except IndexError: + # Some device used non-standard new-protocol(美的乐享三代中央空调?) + _LOGGER.debug(f"Non-standard new-protocol {self.data.hex()}") + return result + + +class MessageResponse(MessageBase): + def __init__(self, message): + super().__init__() + if message is None or len(message) < self.HEADER_LENGTH + 1: + raise MessageLenError + self._header = message[:self.HEADER_LENGTH] + self.device_protocol_version = self._header[8] + self.message_type = self._header[-1] + self.device_type = self._header[2] + body = message[self.HEADER_LENGTH: -1] + self._body = MessageBody(body) + self.body_type = self._body.body_type + + @property + def header(self): + return self._header + + @property + def body(self): + return self._body.data + + def set_body(self, body: MessageBody): + self._body = body + + def set_attr(self): + for key in vars(self._body).keys(): + if key != "data": + value = getattr(self._body, key, None) + setattr(self, key, value) + + +class MessageSubtypeResponse(MessageResponse): + def __init__(self, message): + super().__init__(message) + if self._message_type == MessageType.querySubtype: + body = message[self.HEADER_LENGTH: -1] + self.sub_type = (body[2] if len(body) > 2 else 0) + ((body[3] << 8) if len(body) > 3 else 0) + diff --git a/midea.py b/midea.py index 1d64193..63f5b95 100644 --- a/midea.py +++ b/midea.py @@ -1,16 +1,47 @@ -# import midea_ac_lan.midea.core as midea_core - - - -# devices = midea_core,discover() - import security; +import cloud; +import aiohttp; +import asyncio; +import discover; +import device; +async def test(): + devices = discover.discover() + for device_id in devices: + cl = cloud.MSmartHomeCloud( + "MSmartHome", + aiohttp.ClientSession(), + "michaelh.95@t-online.de", + "Hoda.semi1" + ) -secure = security.LocalSecurity() + if await cl.login(): + keys = await cl.get_keys(device_id) -result = secure.encode_8370(None, security.MSGTYPE_HANDSHAKE_REQUEST) + for k in keys: + token = keys[k]['token'] + key = keys[k]['key'] + + device_info = devices[device_id] + + dev = device.MiedaDevice( + name="", + device_id=device_id, + device_type=225, + ip_address=device_info['ip_address'], + port=device_info['port'], + token=token, + key=key, + protocol=3, + model=device_info['model'], + attributes={} + ) + + if dev.connect(False): + return dev + +dev = asyncio.run(test()) + +print(dev) -print(result) -print(devices) \ No newline at end of file diff --git a/packet_builder.py b/packet_builder.py new file mode 100644 index 0000000..ce2828c --- /dev/null +++ b/packet_builder.py @@ -0,0 +1,60 @@ +from security import LocalSecurity +import datetime + + +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 + 0x5a, 0x5a, + # 2 bytes - mMessageType + 0x01, 0x11, + # 2 bytes - PacketLenght + 0x00, 0x00, + # 2 bytes + 0x20, 0x00, + # 4 bytes - MessageId + 0x00, 0x00, 0x00, 0x00, + # 8 bytes - Date&Time + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + # 6 bytes - mDeviceID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + # 12 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + self.packet[12:20] = self.packet_time() + self.packet[20:28] = device_id.to_bytes(8, "little") + self.command = command + + def finalize(self, msg_type=1): + if msg_type != 1: + self.packet[3] = 0x10 + self.packet[6] = 0x7b + else: + self.packet.extend(self.security.aes_encrypt(self.command)) + # PacketLenght + self.packet[4:6] = (len(self.packet) + 16).to_bytes(2, "little") + # Append a basic checksum data(16 bytes) to the packet + self.packet.extend(self.encode32(self.packet)) + return self.packet + + def encode32(self, data: bytearray): + return self.security.encode32_data(data) + + @staticmethod + def checksum(data): + return (~ sum(data) + 1) & 0xff + + @staticmethod + def packet_time(): + t = datetime.datetime.now().strftime("%Y%m%d%H%M%S%f")[ + :16] + b = bytearray() + for i in range(0, len(t), 2): + d = int(t[i:i+2]) + b.insert(0, d) + return b diff --git a/src/cloud.rs b/src/cloud.rs new file mode 100644 index 0000000..ceefc8d --- /dev/null +++ b/src/cloud.rs @@ -0,0 +1,7 @@ +pub struct Cloud { + // +} + +impl Cloud { + pub const APP_KEY: &str = "ac21b9f9cbfe4ca5a88562ef25e2b768"; +} diff --git a/src/discover.rs b/src/discover.rs index 5292eeb..7e442ce 100644 --- a/src/discover.rs +++ b/src/discover.rs @@ -26,6 +26,7 @@ pub struct Device { info: DeviceInfo, socket: UdpSocket, + security: Security, } impl Device { @@ -36,7 +37,11 @@ impl Device { socket.connect(info.addr)?; - let me = Self { info, socket }; + let mut me = Self { + info, + socket, + security: Security::default(), + }; if me.info.protocol == 3 { me.authenticate()?; @@ -47,8 +52,8 @@ impl Device { Ok(me) } - fn authenticate(&self) -> Result<()> { - let request = Security::encode_8370(MsgType::HANDSHAKE_REQUEST)?; + fn authenticate(&mut self) -> Result<()> { + let request = self.security.encode_8370(MsgType::HANDSHAKE_REQUEST)?; Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 9eaef33..2ac49c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ use std::num::ParseIntError; +mod cloud; mod discover; mod security; diff --git a/src/security.rs b/src/security.rs index c63cc91..a77cabd 100644 --- a/src/security.rs +++ b/src/security.rs @@ -27,7 +27,7 @@ impl Security { const N: u128 = 141661095494369103254425781617665632877; const KEY: [u8; 16] = Self::N.to_be_bytes(); - pub fn decrypt(&self, data: &mut [u8]) -> &[u8] { + pub fn decrypt(data: &mut [u8]) -> &[u8] { let array = GenericArray::from(Self::KEY); let cipher = Aes128::new(&array);