5 Commits

Author SHA1 Message Date
sususweet
15ac35a887 fix: async import module. 2025-11-21 09:49:35 +08:00
sususweet
2858e991ae feat: add support for T0xBF. Fix #36. 2025-11-21 09:39:42 +08:00
Yingqi Tang
fe1a5b315b fix: file async read 2025-11-21 01:16:55 +00:00
Yingqi Tang
849280676f feat: add follow_body_sense for T0xAC. Fixes #51 2025-11-21 01:10:45 +00:00
Yingqi Tang
3edd94d790 fix: refresh status fallback to lua when cloud api is down. Fixes #52. 2025-11-21 01:05:55 +00:00
10 changed files with 118 additions and 35 deletions

View File

@@ -32,6 +32,7 @@ Get devices from MSmartHome/Midea Meiju homes through the network and control th
- T0xB6 Range Hood - T0xB6 Range Hood
- T0xB7 Gas Stove - T0xB7 Gas Stove
- T0xB8 Smart Robot Vacuum - T0xB8 Smart Robot Vacuum
- T0xBF Microwave Steam Oven
- T0xCA French Door Refrigerator - T0xCA French Door Refrigerator
- T0xCC Central Air Conditioning (Ducted) Wi-Fi Controller - T0xCC Central Air Conditioning (Ducted) Wi-Fi Controller
- T0xCD Air Energy Water Heater - T0xCD Air Energy Water Heater

View File

@@ -32,6 +32,7 @@
- T0xB6 抽油烟机 - T0xB6 抽油烟机
- T0xB7 燃气灶 - T0xB7 燃气灶
- T0xB8 智能扫地机器人 - T0xB8 智能扫地机器人
- T0xBF 微波炉
- T0xCA 对开门冰箱 - T0xCA 对开门冰箱
- T0xCC 中央空调(风管机)Wi-Fi线控器 - T0xCC 中央空调(风管机)Wi-Fi线控器
- T0xCD 空气能热水器 - T0xCD 空气能热水器

View File

@@ -1,3 +1,4 @@
import asyncio
import os import os
import base64 import base64
from importlib import import_module from importlib import import_module
@@ -62,6 +63,9 @@ PLATFORMS: list[Platform] = [
Platform.BUTTON Platform.BUTTON
] ]
async def import_module_async(module_name):
# 在线程池中执行导入操作
return await asyncio.to_thread(import_module, module_name, __package__)
def get_sn8_used(hass: HomeAssistant, sn8): def get_sn8_used(hass: HomeAssistant, sn8):
entries = hass.config_entries.async_entries(DOMAIN) entries = hass.config_entries.async_entries(DOMAIN)
@@ -102,7 +106,7 @@ async def load_device_config(hass: HomeAssistant, device_type, sn8):
# if not json_data: # if not json_data:
device_path = f".device_mapping.{'T0x%02X' % device_type}" device_path = f".device_mapping.{'T0x%02X' % device_type}"
try: try:
mapping_module = import_module(device_path, __package__) mapping_module = await import_module_async(device_path)
for key, config in mapping_module.DEVICE_MAPPING.items(): for key, config in mapping_module.DEVICE_MAPPING.items():
# support tuple & regular expression pattern to support multiple sn8 sharing one mapping # support tuple & regular expression pattern to support multiple sn8 sharing one mapping
if (key == sn8) or (isinstance(key, tuple) and sn8 in key) or (isinstance(key, str) and re.match(key, sn8)): if (key == sn8) or (isinstance(key, tuple) and sn8 in key) or (isinstance(key, str) and re.match(key, sn8)):

View File

@@ -4,6 +4,7 @@ import datetime
import json import json
import base64 import base64
import asyncio import asyncio
import aiofiles
import requests import requests
from aiohttp import ClientSession from aiohttp import ClientSession
from secrets import token_hex from secrets import token_hex
@@ -89,12 +90,10 @@ class MideaCloud:
"accesstoken": self._access_token "accesstoken": self._access_token
}) })
response:dict = {"code": -1} response:dict = {"code": -1}
_LOGGER.debug(f"Midea cloud API header: {header}")
_LOGGER.debug(f"Midea cloud API dump_data: {dump_data}")
try: try:
r = await self._session.request(method, url, headers=header, data=dump_data, timeout=5) r = await self._session.request(method, url, headers=header, data=dump_data, timeout=5)
raw = await r.read() raw = await r.read()
_LOGGER.debug(f"Midea cloud API url: {url}, data: {data}, response: {raw}") _LOGGER.debug(f"Midea cloud API url: {url}, header: {header}, data: {data}, response: {raw}")
response = json.loads(raw) response = json.loads(raw)
except Exception as e: except Exception as e:
_LOGGER.debug(f"API request attempt failed: {e}") _LOGGER.debug(f"API request attempt failed: {e}")
@@ -467,8 +466,8 @@ class MeijuCloud(MideaCloud):
self._security.aes_decrypt_with_fixed_key(lua)) self._security.aes_decrypt_with_fixed_key(lua))
stream = stream.replace("\r\n", "\n") stream = stream.replace("\r\n", "\n")
fnm = f"{path}/{response['fileName']}" fnm = f"{path}/{response['fileName']}"
with open(fnm, "w") as fp: async with aiofiles.open(fnm, "w") as fp:
fp.write(stream) await fp.write(stream)
return fnm return fnm
@@ -629,8 +628,8 @@ class MSmartHomeCloud(MideaCloud):
self._security.aes_decrypt_with_fixed_key(lua)) self._security.aes_decrypt_with_fixed_key(lua))
stream = stream.replace("\r\n", "\n") stream = stream.replace("\r\n", "\n")
fnm = f"{path}/{response['fileName']}" fnm = f"{path}/{response['fileName']}"
with open(fnm, "w") as fp: async with aiofiles.open(fnm, "w") as fp:
fp.write(stream) await fp.write(stream)
return fnm return fnm
async def send_cloud(self, appliance_code: int, data: bytearray): async def send_cloud(self, appliance_code: int, data: bytearray):

View File

@@ -291,32 +291,34 @@ class MiedaDevice(threading.Thread):
async def refresh_status(self): async def refresh_status(self):
for query in self._queries: for query in self._queries:
# try:
# if self._lua_runtime is not None:
# if query_cmd := self._lua_runtime.build_query(query):
# await self._build_send(query_cmd)
# return
# except Exception as e:
# traceback.print_exc()
cloud = self._cloud cloud = self._cloud
if cloud and hasattr(cloud, "get_device_status"): if cloud and hasattr(cloud, "get_device_status"):
if isinstance(cloud, MSmartHomeCloud): if isinstance(cloud, MSmartHomeCloud):
status = await cloud.get_device_status( if status := await cloud.get_device_status(
appliance_code=self._device_id, appliance_code=self._device_id,
device_type=self.device_type, device_type=self.device_type,
sn=self.sn, sn=self.sn,
model_number=self.subtype, model_number=self.subtype,
manufacturer_code=self._manufacturer_code, manufacturer_code=self._manufacturer_code,
query=query query=query
) ):
self._parse_cloud_message(status) self._parse_cloud_message(status)
else:
if self._lua_runtime is not None:
if query_cmd := self._lua_runtime.build_query(query):
await self._build_send(query_cmd)
elif isinstance(cloud, MeijuCloud): elif isinstance(cloud, MeijuCloud):
status = await cloud.get_device_status( if status := await cloud.get_device_status(
appliance_code=self._device_id, appliance_code=self._device_id,
query=query query=query
) ):
self._parse_cloud_message(status) self._parse_cloud_message(status)
else:
if self._lua_runtime is not None:
if query_cmd := self._lua_runtime.build_query(query):
await self._build_send(query_cmd)
def _parse_cloud_message(self, status): def _parse_cloud_message(self, status):
# MideaLogger.debug(f"Received: {decrypted}") # MideaLogger.debug(f"Received: {decrypted}")
@@ -415,13 +417,7 @@ class MiedaDevice(threading.Thread):
return ParseMessageResult.SUCCESS return ParseMessageResult.SUCCESS
async def _send_message(self, data): async def _send_message(self, data):
reply = None if reply := await self._cloud.send_cloud(self._device_id, data):
if isinstance(self._cloud, MSmartHomeCloud):
reply = await self._cloud.send_cloud(self._device_id, data)
elif isinstance(self._cloud, MeijuCloud):
reply = await self._cloud.send_cloud(self._device_id, data)
if reply is not None:
if reply_dec := self._lua_runtime.decode_status(dec_string_to_bytes(reply).hex()): if reply_dec := self._lua_runtime.decode_status(dec_string_to_bytes(reply).hex()):
MideaLogger.debug(f"Decoded: {reply_dec}") MideaLogger.debug(f"Decoded: {reply_dec}")
result = self._parse_cloud_message(reply_dec) result = self._parse_cloud_message(reply_dec)

View File

@@ -107,7 +107,10 @@ DEVICE_MAPPING = {
}, },
"aux_heat": { "aux_heat": {
"device_class": SwitchDeviceClass.SWITCH, "device_class": SwitchDeviceClass.SWITCH,
} },
"follow_body_sense": {
"device_class": SwitchDeviceClass.SWITCH,
},
}, },
Platform.SENSOR: { Platform.SENSOR: {
"mode": { "mode": {
@@ -252,16 +255,13 @@ DEVICE_MAPPING = {
}, },
Platform.SWITCH: { Platform.SWITCH: {
"dry": { "dry": {
"name": "干燥",
"device_class": SwitchDeviceClass.SWITCH, "device_class": SwitchDeviceClass.SWITCH,
}, },
"prevent_straight_wind": { "prevent_straight_wind": {
"name": "防直吹",
"device_class": SwitchDeviceClass.SWITCH, "device_class": SwitchDeviceClass.SWITCH,
"rationale": [1, 2] "rationale": [1, 2]
}, },
"aux_heat": { "aux_heat": {
"name": "电辅热",
"device_class": SwitchDeviceClass.SWITCH, "device_class": SwitchDeviceClass.SWITCH,
} }
}, },
@@ -357,7 +357,6 @@ DEVICE_MAPPING = {
}, },
Platform.SWITCH: { Platform.SWITCH: {
"power": { "power": {
"name": "电源",
"device_class": SwitchDeviceClass.SWITCH, "device_class": SwitchDeviceClass.SWITCH,
}, },
}, },

View File

@@ -0,0 +1,65 @@
from homeassistant.const import Platform, UnitOfTime, UnitOfArea, UnitOfTemperature
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
DEVICE_MAPPING = {
"default": {
"rationale": ["off", "on"],
"queries": [{}],
"centralized": [],
"calculate": {
"get": [
{
"lvalue": "[work_time]",
"rvalue": "[work_second] + 60 * [work_minute] + 3600 * [work_hour]"
},
{
"lvalue": "[set_time]",
"rvalue": "[second_set] + 60 * [minute_set] + 3600 * [hour_set]"
}
],
"set": [
]
},
"entities": {
Platform.BINARY_SENSOR: {
"lack_water": {
"device_class": BinarySensorDeviceClass.RUNNING,
"rationale": [0, 1]
},
"door_open": {
"device_class": BinarySensorDeviceClass.RUNNING,
},
"change_water": {
"device_class": BinarySensorDeviceClass.RUNNING,
"rationale": [0, 1]
}
},
Platform.SENSOR: {
"work_status": {
"device_class": SensorDeviceClass.ENUM
},
"cur_temperature_above": {
"device_class": SensorDeviceClass.TEMPERATURE,
"unit_of_measurement": UnitOfTemperature.CELSIUS,
"state_class": SensorStateClass.MEASUREMENT
},
"cur_temperature_underside": {
"device_class": SensorDeviceClass.TEMPERATURE,
"unit_of_measurement": UnitOfTemperature.CELSIUS,
"state_class": SensorStateClass.MEASUREMENT
},
"work_time": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.SECONDS,
"state_class": SensorStateClass.MEASUREMENT
},
"set_time": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.SECONDS,
"state_class": SensorStateClass.MEASUREMENT
},
}
}
}
}

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"issue_tracker": "https://github.com/sususweet/midea-meiju-codec/issues", "issue_tracker": "https://github.com/sususweet/midea-meiju-codec/issues",
"requirements": ["lupa>=2.0"], "requirements": ["lupa>=2.0"],
"version": "v0.1.24" "version": "v0.1.25"
} }

View File

@@ -651,6 +651,9 @@
"move_direction": { "move_direction": {
"name": "Move Direction" "name": "Move Direction"
}, },
"work_status": {
"name": "Work Status"
},
"sub_work_status": { "sub_work_status": {
"name": "Sub Work Status" "name": "Sub Work Status"
}, },
@@ -1206,6 +1209,9 @@
"work_time": { "work_time": {
"name": "Work Time" "name": "Work Time"
}, },
"set_time": {
"name": "Set Time"
},
"zero_cold_tem": { "zero_cold_tem": {
"name": "Zero Cold Temperature" "name": "Zero Cold Temperature"
}, },
@@ -1574,6 +1580,9 @@
} }
}, },
"switch": { "switch": {
"follow_body_sense": {
"name": "Follow Body Sense"
},
"waterions": { "waterions": {
"name": "Disinfection" "name": "Disinfection"
}, },

View File

@@ -655,6 +655,9 @@
"move_direction": { "move_direction": {
"name": "移动方向" "name": "移动方向"
}, },
"work_status": {
"name": "工作状态"
},
"sub_work_status": { "sub_work_status": {
"name": "子工作状态" "name": "子工作状态"
}, },
@@ -1210,6 +1213,9 @@
"work_time": { "work_time": {
"name": "工作时间" "name": "工作时间"
}, },
"set_time": {
"name": "设置工作时间"
},
"zero_cold_tem": { "zero_cold_tem": {
"name": "零冷水温度" "name": "零冷水温度"
}, },
@@ -1578,6 +1584,9 @@
} }
}, },
"switch": { "switch": {
"follow_body_sense": {
"name": "随身感"
},
"waterions": { "waterions": {
"name": "消杀" "name": "消杀"
}, },