This commit is contained in:
unknown
2023-09-17 19:40:54 +08:00
parent df4d25f5d0
commit 7263e09692
16 changed files with 1595 additions and 509 deletions

View File

@@ -1,204 +1,465 @@
import datetime
import time
import json
import logging
from aiohttp import ClientSession
from secrets import token_hex, token_urlsafe
import time
import datetime
import json
import base64
from threading import Lock
from .security import CloudSecurity
CLIENT_TYPE = 1 # Android
FORMAT = 2 # JSON
APP_KEY = "4675636b"
from aiohttp import ClientSession
from secrets import token_hex
from .security import CloudSecurity, MeijuCloudSecurity, MSmartCloudSecurity
_LOGGER = logging.getLogger(__name__)
clouds = {
"美的美居": {
"class_name": "MeijuCloud",
"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_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=",
},
}
class MideaCloudBase:
LANGUAGE = "en_US"
APP_ID = "1010"
SRC = "1010"
LOGIN_KEY = None
IOT_KEY = None
DEVICE_ID = int(time.time() * 100000)
default_keys = {
99: {
"token": "ee755a84a115703768bcc7c6c13d3d629aa416f1e2fd798beb9f78cbb1381d09"
"1cc245d7b063aad2a900e5b498fbd936c811f5d504b2e656d4f33b3bbc6d1da3",
"key": "ed37bd31558a4b039aaf4e7a7a59aa7a75fd9101682045f69baf45d28380ae5c"
}
}
def __init__(self, session: ClientSession, security, username: str, password: str, server: str = None):
self.session = session
self.username = username
self.password = password
self.server = None
self.login_id = None
self.access_token = ""
self.key = None
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.login_session = None
self.security = security
self.server = server
self._app_key = app_key
self._account = account
self._password = password
self._api_url = api_url
self._access_token = None
self._login_id = None
async def api_request(self, endpoint, args=None, data=None) -> dict | None:
args = args or {}
headers = {}
if data is None:
data = {
"appId": self.APP_ID,
"format": FORMAT,
"clientType": CLIENT_TYPE,
"language": self.LANGUAGE,
"src": self.SRC,
"stamp": datetime.datetime.now().strftime("%Y%m%d%H%M%S"),
"deviceId": self.DEVICE_ID,
}
data.update(args)
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),
"reqId": token_hex(16)
})
if not data.get("stamp"):
data.update({
"stamp": datetime.datetime.now().strftime("%Y%m%d%H%M%S")
})
url = self.server + endpoint
random = str(int(time.time()))
sign = self.security.sign(json.dumps(data), random)
headers.update({
"Content-Type": "application/json",
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,
"accessToken": self.access_token
})
response = {"code": -1}
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=headers, data=json.dumps(data), timeout=10)
r = await self._session.request("POST", url, headers=header, data=dump_data, timeout=10)
raw = await r.read()
_LOGGER.debug(f"Endpoint: {endpoint}, Response: {str(raw)}")
_LOGGER.debug(f"Midea cloud API url: {url}, data: {data}, response: {raw}")
response = json.loads(raw)
break
except Exception as e:
_LOGGER.error(f"Cloud error: {repr(e)}")
pass
if int(response["code"]) == 0 and "data" in response:
return response["data"]
print(response)
return None
async def get_login_id(self):
response = await self.api_request(
"/v1/user/login/id/get",
args={"loginAccount": self.username}
)
if response:
self.login_id = response["loginId"]
return True
return False
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):
result = await self.get_login_id()
if result:
response = await self.api_request(
"/mj/user/login",
data={
"data": {
"appKey": APP_KEY,
"platform": FORMAT,
"deviceId": self.DEVICE_ID
},
"iotData": {
"appId": self.APP_ID,
"clientType": CLIENT_TYPE,
"iampwd": self.security.encrypt_iam_password(self.login_id, self.password),
"loginAccount": self.username,
"password": self.security.encrypt_password(self.login_id, self.password),
"pushToken": token_urlsafe(120),
"reqId": token_hex(16),
"src": self.SRC,
"stamp": datetime.time().strftime("%Y%m%d%H%M%S"),
},
}
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:
self.access_token = response["mdata"]["accessToken"]
if "key" in response:
self.key = CloudSecurity.decrypt(bytes.fromhex(response["key"]))
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
async def list_home(self) -> dict | None:
return {1: "My home"}
async def list_appliances(self, home_id) -> dict | None:
raise NotImplementedError()
async def download_lua(
self, path: str,
device_type: str,
sn: str,
model_number: str | None,
manufacturer_code: str = "0000",
):
raise NotImplementedError()
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]["login_key"],
iot_key=clouds[cloud_name]["iot_key"],
hmac_key=clouds[cloud_name]["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
async def get_token(self, device_id: int, byte_order_big=False):
if byte_order_big:
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 "
f"with byte order '{'big' if byte_order_big else 'little'}': {udpid}")
response = await self.api_request(
"/v1/iot/secure/getToken",
args={"udpid": udpid}
)
if response and "tokenlist" in response:
for token in response["tokenlist"]:
if token["udpId"] == udpid:
return token["token"].upper(), token["key"].upper()
return None, None
async def list_home(self):
if response := await self._api_request(
endpoint="/v1/homegroup/list/get",
data={}
):
homes = {}
for home in response["homeList"]:
homes.update({
int(home["homegroupId"]): home["name"]
})
return homes
return None
async def list_appliances(self, home_id) -> dict | None:
data = {
"homegroupId": home_id
}
if response := await self._api_request(
endpoint="/v1/appliance/home/list/get",
data=data
):
appliances = {}
for home in response.get("homeList") or []:
for room in home.get("roomList") or []:
for appliance in room.get("applianceList"):
device_info = {
"name": appliance.get("name"),
"type": int(appliance.get("type"), 16),
"sn": self._security.aes_decrypt(appliance.get("sn")) if appliance.get("sn") else "",
"sn8": appliance.get("sn8", "00000000"),
"model_number": appliance.get("modelNumber", "0"),
"manufacturer_code":appliance.get("enterpriseCode", "0000"),
"model": appliance.get("productModel"),
"online": appliance.get("onlineStatus") == "1",
}
if device_info.get("sn8") is None or len(device_info.get("sn8")) == 0:
device_info["sn8"] = "00000000"
if device_info.get("model") is None or len(device_info.get("model")) == 0:
device_info["model"] = device_info["sn8"]
appliances[int(appliance["applianceCode"])] = device_info
return appliances
return None
class MeijuCloudExtend(MideaCloudBase):
LANGUAGE = "zh_CN"
LOGIN_KEY = "ad0ee21d48a64bf49f4fb583ab76e799"
IOT_KEY = "prod_secret123@muc"
SERVER = "https://mp-prod.smartmidea.net/mas/v5/app/proxy?alias="
def __init__(self, session: ClientSession, username: str, password: str):
super().__init__(session=session,
security=CloudSecurity(self.IOT_KEY, self.LOGIN_KEY),
username=username,
password=password,
server=self.SERVER)
async def get_homegroups(self):
response = await self.api_request("/v1/homegroup/list/get", args={})
return response.get("homeList")
async def get_devices(self, homegroupID=None):
if homegroupID is None:
homes = []
homegroups = await self.get_homegroups()
if homegroups:
for home in homegroups:
homes.append(home["homegroupId"])
else:
homes = [homegroupID]
devices = []
for home in homes:
response = await self.api_request("/v1/appliance/home/list/get", args={
'homegroupId': home
})
if response:
for h in response.get("homeList") or []:
for r in h.get("roomList") or []:
for a in r.get("applianceList"):
a["sn"] = CloudSecurity.decrypt(bytes.fromhex(a["sn"]), self.key).decode()
devices.append(a)
return devices
async def get_lua(self, sn, device_type, path, enterprise_code=None):
response = await self.api_request(
"/v1/appliance/protocol/lua/luaGet",
data={
"applianceSn": sn,
"applianceType": "0x%02X" % device_type,
"applianceMFCode": enterprise_code if enterprise_code else "0000",
'version': "0",
"iotAppId": "900",
"stamp": datetime.datetime.now().strftime("%Y%m%d%H%M%S")
}
)
async def download_lua(
self, path: str,
device_type: int,
sn: str,
model_number: str | None,
manufacturer_code: str = "0000",
):
data = {
"applianceSn": sn,
"applianceType": "0x%02X" % device_type,
"applianceMFCode": manufacturer_code,
'version': "0",
"iotAppId": self.APP_ID,
}
fnm = None
if response:
res = await self.session.get(response["url"])
if response := await self._api_request(
endpoint="/v1/appliance/protocol/lua/luaGet",
data=data
):
res = await self._session.get(response["url"])
if res.status == 200:
lua = await res.text()
if lua:
stream = 'local bit = require "bit" ' + CloudSecurity.decrypt(bytes.fromhex(lua)).decode()
stream = ('local bit = require "bit"\n' +
self._security.aes_decrypt_with_fixed_key(lua))
stream = stream.replace("\r\n", "\n")
fnm = f"{path}/{response['fileName']}"
with open(fnm, "w") as fp:
fp.write(stream)
return fnm
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]["app_key"],
iot_key=clouds[cloud_name]["iot_key"],
hmac_key=clouds[cloud_name]["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
async def list_appliances(self, home_id=None) -> dict | None:
data = self._make_general_data()
if response := await self._api_request(
endpoint="/v1/appliance/user/list/get",
data=data
):
appliances = {}
for appliance in response["list"]:
device_info = {
"name": appliance.get("name"),
"type": int(appliance.get("type"), 16),
"sn": self._security.aes_decrypt(appliance.get("sn")) if appliance.get("sn") else "",
"sn8": "",
"model_number": appliance.get("modelNumber", "0"),
"manufacturer_code":appliance.get("enterpriseCode", "0000"),
"model": "",
"online": appliance.get("onlineStatus") == "1",
}
device_info["sn8"] = device_info.get("sn")[9:17] if len(device_info["sn"]) > 17 else ""
device_info["model"] = device_info.get("sn8")
appliances[int(appliance["id"])] = device_info
return appliances
return None
async def download_lua(
self, path: str,
device_type: int,
sn: str,
model_number: str | None,
manufacturer_code: str = "0000",
):
data = {
"clientType": "1",
"appId": self.APP_ID,
"format": "2",
"deviceId": self._device_id,
"iotAppId": self.APP_ID,
"applianceMFCode": manufacturer_code,
"applianceType": "0x%02X" % device_type,
"modelNumber": model_number,
"applianceSn": self._security.aes_encrypt_with_fixed_key(sn.encode("ascii")).hex(),
"version": "0",
"encryptedType ": "2"
}
fnm = None
if response := await self._api_request(
endpoint="/v2/luaEncryption/luaGet",
data=data
):
res = await self._session.get(response["url"])
if res.status == 200:
lua = await res.text()
if lua:
stream = ('local bit = require "bit"\n' +
self._security.aes_decrypt_with_fixed_key(lua))
stream = stream.replace("\r\n", "\n")
fnm = f"{path}/{response['fileName']}"
with open(fnm, "w") as fp:
fp.write(stream)
return fnm
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

View File

@@ -5,6 +5,7 @@ from enum import IntEnum
from .security import LocalSecurity, MSGTYPE_HANDSHAKE_REQUEST, MSGTYPE_ENCRYPTED_REQUEST
from .packet_builder import PacketBuilder
from .lua_runtime import MideaCodec
from .message import MessageQuestCustom
from .logger import MideaLogger
@@ -70,6 +71,8 @@ class MiedaDevice(threading.Thread):
self._connected = False
self._queries = [{}]
self._centralized = []
self._calculate_get = []
self._calculate_set = []
self._lua_runtime = MideaCodec(lua_file, sn=sn, subtype=subtype) if lua_file is not None else None
@property
@@ -111,22 +114,18 @@ class MiedaDevice(threading.Thread):
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):
def set_queries(self, queries: list):
self._queries = queries
@property
def centralized(self):
return self._centralized
@centralized.setter
def centralized(self, centralized: list):
def set_centralized(self, centralized: list):
self._centralized = centralized
def set_calculate(self, calculate: dict):
values_get = calculate.get("get")
values_set = calculate.get("set")
self._calculate_get = values_get if values_get else []
self._calculate_set = values_set if values_set else []
def get_attribute(self, attribute):
return self._attributes.get(attribute)
@@ -137,7 +136,7 @@ class MiedaDevice(threading.Thread):
new_status[attr] = self._attributes.get(attr)
new_status[attribute] = value
if set_cmd := self._lua_runtime.build_control(new_status):
self.build_send(set_cmd)
self._build_send(set_cmd)
def set_attributes(self, attributes):
new_status = {}
@@ -150,22 +149,26 @@ class MiedaDevice(threading.Thread):
new_status[attribute] = value
if has_new:
if set_cmd := self._lua_runtime.build_control(new_status):
self.build_send(set_cmd)
self._build_send(set_cmd)
@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 set_ip_address(self, ip_address):
MideaLogger.debug(f"Update IP address to {ip_address}")
self._ip_address = ip_address
self.close_socket()
def send_command(self, cmd_type, cmd_body: bytearray):
cmd = MessageQuestCustom(self._device_type, cmd_type, cmd_body)
try:
self._build_send(cmd.serialize().hex())
except socket.error as e:
MideaLogger.debug(
f"Interface send_command failure, {repr(e)}, "
f"cmd_type: {cmd_type}, cmd_body: {cmd_body.hex()}",
self._device_id
)
def register_update(self, update):
self._updates.append(update)
def connect(self, refresh=False):
try:
@@ -175,11 +178,11 @@ class MiedaDevice(threading.Thread):
self._socket.connect((self._ip_address, self._port))
MideaLogger.debug(f"Connected", self._device_id)
if self._protocol == 3:
self.authenticate()
self._authenticate()
MideaLogger.debug(f"Authentication success", self._device_id)
self.device_connected(True)
self._device_connected(True)
if refresh:
self.refresh_status()
self._refresh_status()
return True
except socket.timeout:
MideaLogger.debug(f"Connection timed out", self._device_id)
@@ -194,10 +197,34 @@ class MiedaDevice(threading.Thread):
except Exception as e:
MideaLogger.error(f"Unknown error: {e.__traceback__.tb_frame.f_globals['__file__']}, "
f"{e.__traceback__.tb_lineno}, {repr(e)}")
self.device_connected(False)
if refresh:
self._device_connected(False)
self._socket = None
return False
def authenticate(self):
def disconnect(self):
self._buffer = b""
if self._socket:
self._socket.close()
self._socket = None
@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 _authenticate(self):
request = self._security.encode_8370(
self._token, MSGTYPE_HANDSHAKE_REQUEST)
MideaLogger.debug(f"Handshaking")
@@ -208,34 +235,34 @@ class MiedaDevice(threading.Thread):
response = response[8: 72]
self._security.tcp_key(response, self._key)
def send_message(self, data):
def _send_message(self, data):
if self._protocol == 3:
self.send_message_v3(data, msg_type=MSGTYPE_ENCRYPTED_REQUEST)
self._send_message_v3(data, msg_type=MSGTYPE_ENCRYPTED_REQUEST)
else:
self.send_message_v2(data)
self._send_message_v2(data)
def send_message_v2(self, data):
def _send_message_v2(self, data):
if self._socket is not None:
self._socket.send(data)
else:
MideaLogger.debug(f"Send failure, device disconnected, data: {data.hex()}")
MideaLogger.debug(f"Command send failure, device disconnected, data: {data.hex()}")
def send_message_v3(self, data, msg_type=MSGTYPE_ENCRYPTED_REQUEST):
def _send_message_v3(self, data, msg_type=MSGTYPE_ENCRYPTED_REQUEST):
data = self._security.encode_8370(data, msg_type)
self.send_message_v2(data)
self._send_message_v2(data)
def build_send(self, cmd):
MideaLogger.debug(f"Sending: {cmd}")
def _build_send(self, cmd: str):
MideaLogger.debug(f"Sending: {cmd.lower()}")
bytes_cmd = bytes.fromhex(cmd)
msg = PacketBuilder(self._device_id, bytes_cmd).finalize()
self.send_message(msg)
self._send_message(msg)
def refresh_status(self):
def _refresh_status(self):
for query in self._queries:
if query_cmd := self._lua_runtime.build_query(query):
self.build_send(query_cmd)
self._build_send(query_cmd)
def parse_message(self, msg):
def _parse_message(self, msg):
if self._protocol == 3:
messages, self._buffer = self._security.decode_8370(self._buffer + msg)
else:
@@ -254,8 +281,7 @@ class MiedaDevice(threading.Thread):
cryptographic = message[40:-16]
if payload_len % 16 == 0:
decrypted = self._security.aes_decrypt(cryptographic)
MideaLogger.debug(f"Received: {decrypted.hex()}")
# 这就是最终消息
MideaLogger.debug(f"Received: {decrypted.hex().lower()}")
if status := self._lua_runtime.decode_status(decrypted.hex()):
MideaLogger.debug(f"Decoded: {status}")
new_status = {}
@@ -265,22 +291,44 @@ class MiedaDevice(threading.Thread):
self._attributes[single] = value
new_status[single] = value
if len(new_status) > 0:
self.update_all(new_status)
for c in self._calculate_get:
lvalue = c.get("lvalue")
rvalue = c.get("rvalue")
if lvalue and rvalue:
calculate = False
for s, v in new_status.items():
if rvalue.find(f"[{s}]") >= 0:
calculate = True
break
if calculate:
calculate_str1 = \
(f"{lvalue.replace('[', 'self._attributes[')} = "
f"{rvalue.replace('[', 'self._attributes[')}") \
.replace("[","[\"").replace("]","\"]")
calculate_str2 = \
(f"{lvalue.replace('[', 'new_status[')} = "
f"{rvalue.replace('[', 'self._attributes[')}") \
.replace("[","[\"").replace("]","\"]")
try:
exec(calculate_str1)
exec(calculate_str2)
except Exception:
MideaLogger.warning(
f"Calculation Error: {lvalue} = {rvalue}", self._device_id
)
self._update_all(new_status)
return ParseMessageResult.SUCCESS
def send_heartbeat(self):
def _send_heartbeat(self):
msg = PacketBuilder(self._device_id, bytearray([0x00])).finalize(msg_type=0)
self.send_message(msg)
self._send_message(msg)
def device_connected(self, connected=True):
def _device_connected(self, connected=True):
self._connected = connected
status = {"connected": connected}
self.update_all(status)
self._update_all(status)
def register_update(self, update):
self._updates.append(update)
def update_all(self, status):
def _update_all(self, status):
MideaLogger.debug(f"Status update: {status}")
for update in self._updates:
update(status)
@@ -294,18 +342,7 @@ class MiedaDevice(threading.Thread):
if self._is_run:
self._is_run = False
self._lua_runtime = None
self.close_socket()
def close_socket(self):
self._buffer = b""
if self._socket:
self._socket.close()
self._socket = None
def set_ip_address(self, ip_address):
MideaLogger.debug(f"Update IP address to {ip_address}")
self._ip_address = ip_address
self.close_socket()
self.disconnect()
def run(self):
while self._is_run:
@@ -313,7 +350,7 @@ class MiedaDevice(threading.Thread):
if self.connect(refresh=True) is False:
if not self._is_run:
return
self.close_socket()
self.disconnect()
time.sleep(5)
timeout_counter = 0
start = time.time()
@@ -323,20 +360,20 @@ class MiedaDevice(threading.Thread):
while True:
try:
now = time.time()
if now - previous_refresh >= self._refresh_interval:
self.refresh_status()
if 0 < self._refresh_interval <= now - previous_refresh:
self._refresh_status()
previous_refresh = now
if now - previous_heartbeat >= self._heartbeat_interval:
self.send_heartbeat()
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)
result = self._parse_message(msg)
if result == ParseMessageResult.ERROR:
MideaLogger.debug(f"Message 'ERROR' received")
self.close_socket()
self.disconnect()
break
elif result == ParseMessageResult.SUCCESS:
timeout_counter = 0
@@ -344,16 +381,16 @@ class MiedaDevice(threading.Thread):
timeout_counter = timeout_counter + 1
if timeout_counter >= 120:
MideaLogger.debug(f"Heartbeat timed out")
self.close_socket()
self.disconnect()
break
except socket.error as e:
MideaLogger.debug(f"Socket error {repr(e)}")
self.close_socket()
self.disconnect()
break
except Exception as e:
MideaLogger.error(f"Unknown error :{e.__traceback__.tb_frame.f_globals['__file__']}, "
f"{e.__traceback__.tb_lineno}, {repr(e)}")
self.close_socket()
self.disconnect()
break

View File

@@ -107,6 +107,8 @@ def discover(discover_type=None, ip_address=None):
MideaLogger.debug(f"Found a supported device: {device}")
else:
MideaLogger.debug(f"Found a unsupported device: {device}")
if ip_address is not None:
break
except socket.timeout:
break
except socket.error as e:

View File

@@ -2,7 +2,7 @@ import lupa
import threading
import json
from .logger import MideaLogger
lupa.LuaMemoryError
class LuaRuntime:
def __init__(self, file):
@@ -13,14 +13,15 @@ class LuaRuntime:
self._json_to_data = self._runtimes.eval("function(param) return jsonToData(param) end")
self._data_to_json = self._runtimes.eval("function(param) return dataToJson(param) end")
def json_to_data(self, json):
def json_to_data(self, json_value):
with self._lock:
result = self._json_to_data(json)
result = self._json_to_data(json_value)
return result
def data_to_json(self, data):
def data_to_json(self, data_value):
with self._lock:
result = self._data_to_json(data)
result = self._data_to_json(data_value)
return result

View File

@@ -1,85 +1,170 @@
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
import hmac
MSGTYPE_HANDSHAKE_REQUEST = 0x0
MSGTYPE_HANDSHAKE_RESPONSE = 0x1
MSGTYPE_ENCRYPTED_RESPONSE = 0x3
MSGTYPE_ENCRYPTED_REQUEST = 0x6
_LOGGER = logging.getLogger(__name__)
class CloudSecurity:
def __init__(self, iotKey, loginKey):
self._hmackey = "PROD_VnoClJI9aikS8dyy"
self._iotkey = iotKey
self._loginKey = loginKey
def __init__(self, login_key, iot_key, hmac_key, fixed_key=None, fixed_iv=None):
self._login_key = login_key
self._iot_key = iot_key
self._hmac_key = hmac_key
self._aes_key = None
self._aes_iv = None
self._fixed_key = format(fixed_key, 'x').encode("ascii") if fixed_key else None
self._fixed_iv = format(fixed_iv, 'x').encode("ascii") if fixed_iv else None
def sign(self, data: str, random: str) -> str:
msg = self._iotkey
if data:
msg += data
msg = self._iot_key
msg += data
msg += random
sign = hmac.new(self._hmackey.encode("ascii"), msg.encode("ascii"), sha256)
sign = hmac.new(self._hmac_key.encode("ascii"), msg.encode("ascii"), sha256)
return sign.hexdigest()
def encrypt_password(self, loginId, data):
def encrypt_password(self, login_id, data):
m = sha256()
m.update(data.encode("ascii"))
login_hash = loginId + m.hexdigest() + self._loginKey
login_hash = login_id + m.hexdigest() + self._login_key
m = sha256()
m.update(login_hash.encode("ascii"))
return m.hexdigest()
def encrypt_iam_password(self, login_id, data) -> str:
raise NotImplementedError
def encrypt_iam_password(self, loginId, data) -> str:
@staticmethod
def get_deviceid(username):
return md5(f"Hello, {username}!".encode("ascii")).digest().hex()[:16]
@staticmethod
def get_udp_id(appliance_id, method=0):
if method == 0:
bytes_id = bytes(reversed(appliance_id.to_bytes(8, "big")))
elif method == 1:
bytes_id = appliance_id.to_bytes(6, "big")
elif method == 2:
bytes_id = appliance_id.to_bytes(6, "little")
else:
return None
data = bytearray(sha256(bytes_id).digest())
for i in range(0, 16):
data[i] ^= data[i + 16]
return data[0: 16].hex()
def set_aes_keys(self, key, iv):
if isinstance(key, str):
key = key.encode("ascii")
if isinstance(iv, str):
iv = iv.encode("ascii")
self._aes_key = key
self._aes_iv = iv
def aes_encrypt_with_fixed_key(self, data):
return self.aes_encrypt(data, self._fixed_key, self._fixed_iv)
def aes_decrypt_with_fixed_key(self, data):
return self.aes_decrypt(data, self._fixed_key, self._fixed_iv)
def aes_encrypt(self, data, key=None, iv=None):
if key is not None:
aes_key = key
aes_iv = iv
else:
aes_key = self._aes_key
aes_iv = self._aes_iv
if aes_key is None:
raise ValueError("Encrypt need a key")
if isinstance(data, str):
data = bytes.fromhex(data)
if aes_iv is None: # ECB
return AES.new(aes_key, AES.MODE_ECB).encrypt(pad(data, 16))
else: # CBC
return AES.new(aes_key, AES.MODE_CBC, iv=aes_iv).encrypt(pad(data, 16))
def aes_decrypt(self, data, key=None, iv=None):
if key is not None:
aes_key = key
aes_iv = iv
else:
aes_key = self._aes_key
aes_iv = self._aes_iv
if aes_key is None:
raise ValueError("Encrypt need a key")
if isinstance(data, str):
data = bytes.fromhex(data)
if aes_iv is None: # ECB
return unpad(AES.new(aes_key, AES.MODE_ECB).decrypt(data), len(aes_key)).decode()
else: # CBC
return unpad(AES.new(aes_key, AES.MODE_CBC, iv=aes_iv).decrypt(data), len(aes_key)).decode()
class MeijuCloudSecurity(CloudSecurity):
def __init__(self, login_key, iot_key, hmac_key):
super().__init__(login_key, iot_key, hmac_key,
10864842703515613082)
def encrypt_iam_password(self, login_id, data) -> str:
md = md5()
md.update(data.encode("ascii"))
md_second = md5()
md_second.update(md.hexdigest().encode("ascii"))
return md_second.hexdigest()
@staticmethod
def get_udpid(data):
data = bytearray(sha256(data).digest())
for i in range(0, 16):
data[i] ^= data[i + 16]
return data[0: 16].hex()
@staticmethod
def decrypt(data:bytes, key:bytes="96c7acdfdb8af79a".encode()):
return unpad(AES.new(key, AES.MODE_ECB).decrypt(data), 16)
class MSmartCloudSecurity(CloudSecurity):
def __init__(self, login_key, iot_key, hmac_key):
super().__init__(login_key, iot_key, hmac_key,
13101328926877700970,
16429062708050928556)
@staticmethod
def encrypt(data:bytes, key:bytes="96c7acdfdb8af79a".encode()):
return AES.new(key, AES.MODE_ECB).encrypt(pad(data, 16))
def encrypt_iam_password(self, login_id, data) -> str:
md = md5()
md.update(data.encode("ascii"))
md_second = md5()
md_second.update(md.hexdigest().encode("ascii"))
login_hash = login_id + md_second.hexdigest() + self._login_key
sha = sha256()
sha.update(login_hash.encode("ascii"))
return sha.hexdigest()
def set_aes_keys(self, encrypted_key, encrypted_iv):
key_digest = sha256(self._login_key.encode("ascii")).hexdigest()
tmp_key = key_digest[:16].encode("ascii")
tmp_iv = key_digest[16:32].encode("ascii")
self._aes_key = self.aes_decrypt(encrypted_key, tmp_key, tmp_iv).encode('ascii')
self._aes_iv = self.aes_decrypt(encrypted_iv, tmp_key, tmp_iv).encode('ascii')
class LocalSecurity:
def __init__(self):
self.blockSize = 16
self.iv = b"\0" * 16
self.aes_key = bytes.fromhex("6a92ef406bad2f0359baad994171ea6d")
self.salt = bytes.fromhex("78686469776a6e6368656b6434643531326368646a783564386534633339344432443753")
self.aes_key = bytes.fromhex(
format(141661095494369103254425781617665632877, 'x')
)
self.salt = bytes.fromhex(
format(233912452794221312800602098970898185176935770387238278451789080441632479840061417076563, 'x')
)
self._tcp_key = None
self._request_count = 0
self._response_count = 0
def aes_decrypt(self, raw):
try:
return unpad(AES.new(self.aes_key, AES.MODE_ECB).decrypt(bytearray(raw)), self.blockSize)
return unpad(AES.new(self.aes_key, AES.MODE_ECB).decrypt(bytearray(raw)), 16)
except ValueError as e:
_LOGGER.error(f"Error in aes_decrypt: {repr(e)} - data: {raw.hex()}")
return bytearray(0)
return bytearray(0)
def aes_encrypt(self, raw):
return AES.new(self.aes_key, AES.MODE_ECB).encrypt(bytearray(pad(raw, self.blockSize)))
return AES.new(self.aes_key, AES.MODE_ECB).encrypt(bytearray(pad(raw, 16)))
def aes_cbc_decrypt(self, raw, key):
return AES.new(key=key, mode=AES.MODE_CBC, iv=self.iv).decrypt(raw)
@@ -155,4 +240,4 @@ class LocalSecurity:
if leftover:
packets, incomplete = self.decode_8370(leftover)
return [data] + packets, incomplete
return [data], b""
return [data], b""