9 Commits

Author SHA1 Message Date
sususweet
730f7d0c7b feat: update version 2025-11-19 02:11:39 +08:00
sususweet
b2a2c80133 feat: update wash function for T0xED. 2025-11-19 02:06:47 +08:00
sususweet
cbaf99592f fix: get_status attributes missing. 2025-11-19 01:59:39 +08:00
sususweet
6062b0534f feat: add support for MSmartHome App. 2025-11-19 01:48:11 +08:00
sususweet
5369e6c13d feat: version 0.1.23 2025-11-14 21:15:14 +08:00
sususweet
2a91ce9914 fix: add entities for T0xB6. Ref #45. 2025-11-14 21:14:11 +08:00
sususweet
52c10f7d0d fix: multi-point login and control for meiju app. 2025-11-14 20:52:03 +08:00
sususweet
c090edfb3d fix: bubble attribute for T0xE3. 2025-11-14 20:51:46 +08:00
sususweet
e3a1812f9a feat: fix cold water single loop support for T0xE3. 2025-11-13 10:09:40 +08:00
10 changed files with 181 additions and 113 deletions

View File

@@ -85,6 +85,7 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
self._key_target_humidity = self._config.get("target_humidity")
self._attr_temperature_unit = self._config.get("temperature_unit")
self._attr_precision = self._config.get("precision")
self._attr_target_temperature_step = self._config.get("precision")
@property
def supported_features(self):

View File

@@ -162,28 +162,6 @@ class MideaCloud:
async def login(self) -> bool:
raise NotImplementedError()
async def send_cloud(self, appliance_id: int, data: bytearray):
appliance_code = str(appliance_id)
params = {
'applianceCode': appliance_code,
'order': self._security.aes_encrypt(bytes_to_dec_string(data)).hex(),
'timestamp': 'true',
"isFull": "false"
}
if response := await self._api_request(
endpoint='/v1/appliance/transparent/send',
data=params,
):
if response and response.get('reply'):
_LOGGER.debug("[%s] Cloud command response: %s", appliance_code, response)
reply_data = self._security.aes_decrypt(bytes.fromhex(response['reply']))
return reply_data
else:
_LOGGER.warning("[%s] Cloud command failed: %s", appliance_code, response)
return None
async def list_home(self) -> dict | None:
return {1: "My home"}
@@ -198,10 +176,6 @@ class MideaCloud:
manufacturer_code: str = "0000",
):
raise NotImplementedError()
async def send_device_control(self, appliance_code: int, control: dict, status: dict | None = None) -> bool:
"""Send control to a device via cloud. Subclasses should implement if supported."""
raise NotImplementedError()
async def send_central_ac_control(self, appliance_code: int, nodeid: str, modelid: str, idtype: int, control: dict) -> bool:
"""Send control to central AC subdevice. Subclasses should implement if supported."""
@@ -323,6 +297,28 @@ class MeijuCloud(MideaCloud):
return appliances
return None
async def send_cloud(self, appliance_id: int, data: bytearray):
appliance_code = str(appliance_id)
params = {
'applianceCode': appliance_code,
'order': self._security.aes_encrypt(bytes_to_dec_string(data)).hex(),
'timestamp': 'true',
"isFull": "false"
}
if response := await self._api_request(
endpoint='/v1/appliance/transparent/send',
data=params,
):
if response and response.get('reply'):
_LOGGER.debug("[%s] Cloud command response: %s", appliance_code, response)
reply_data = self._security.aes_decrypt(bytes.fromhex(response['reply']))
return reply_data
else:
_LOGGER.warning("[%s] Cloud command failed: %s", appliance_code, response)
return None
async def get_device_status(self, appliance_code: int, query: dict) -> dict | None:
data = {
"applianceCode": str(appliance_code),
@@ -637,6 +633,32 @@ class MSmartHomeCloud(MideaCloud):
fp.write(stream)
return fnm
async def send_cloud(self, appliance_code: int, data: bytearray):
appliance_code = str(appliance_code)
params = {
"clientType": "1",
"appId": self.APP_ID,
"format": "2",
"deviceId": self._device_id,
"applianceCode": appliance_code,
'order': self._security.aes_encrypt(bytes_to_dec_string(data)).hex(),
'timestamp': 'true',
"isFull": "false"
}
if response := await self._api_request(
endpoint='/v1/appliance/transparent/send',
data=params,
):
if response and response.get('reply'):
_LOGGER.debug("[%s] Cloud command response: %s", appliance_code, response)
reply_data = self._security.aes_decrypt(bytes.fromhex(response['reply']))
return reply_data
else:
_LOGGER.warning("[%s] Cloud command failed: %s", appliance_code, response)
return None
async def get_device_status(
self,
appliance_code: int,
@@ -671,9 +693,29 @@ class MSmartHomeCloud(MideaCloud):
return response
return None
async def send_device_control(self, appliance_code: int, control: dict, status: dict | None = None) -> bool:
async def send_device_control(
self,
appliance_code: int,
device_type: int,
sn: str,
model_number: str | None,
manufacturer_code: str = "0000",
control: dict | None = None,
status: dict | None = None
) -> bool:
data = {
"applianceCode": str(appliance_code),
"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",
"applianceCode": appliance_code,
"command": {
"control": control
}

View File

@@ -3,7 +3,7 @@ import socket
import traceback
from enum import IntEnum
from .cloud import MideaCloud, MSmartHomeCloud
from .cloud import MideaCloud, MSmartHomeCloud, MeijuCloud
from .security import LocalSecurity, MSGTYPE_HANDSHAKE_REQUEST, MSGTYPE_ENCRYPTED_REQUEST
from .packet_builder import PacketBuilder
from .message import MessageQuestCustom
@@ -180,7 +180,17 @@ class MiedaDevice(threading.Thread):
cloud = self._cloud
if cloud and hasattr(cloud, "send_device_control"):
await cloud.send_device_control(self._device_id, control=nested_status, status=self._attributes)
if isinstance(cloud, MSmartHomeCloud):
await cloud.send_device_control(
appliance_code=self._device_id,
device_type=self.device_type,
sn=self.sn,
model_number=self.subtype,
manufacturer_code=self._manufacturer_code,
control=nested_status,
status=self._attributes)
elif isinstance(cloud, MeijuCloud):
await cloud.send_device_control(self._device_id, control=nested_status, status=self._attributes)
async def set_attributes(self, attributes):
new_status = {}
@@ -207,7 +217,17 @@ class MiedaDevice(threading.Thread):
cloud = self._cloud
if cloud and hasattr(cloud, "send_device_control"):
await cloud.send_device_control(self._device_id, control=nested_status, status=self._attributes)
if isinstance(cloud, MSmartHomeCloud):
await cloud.send_device_control(
appliance_code=self._device_id,
device_type=self.device_type,
sn=self.sn,
model_number=self.subtype,
manufacturer_code=self._manufacturer_code,
control=nested_status,
status=self._attributes)
elif isinstance(cloud, MeijuCloud):
await cloud.send_device_control(self._device_id, control=nested_status, status=self._attributes)
def set_ip_address(self, ip_address):
MideaLogger.debug(f"Update IP address to {ip_address}")
@@ -271,6 +291,14 @@ class MiedaDevice(threading.Thread):
async def refresh_status(self):
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
if cloud and hasattr(cloud, "get_device_status"):
if isinstance(cloud, MSmartHomeCloud):
@@ -282,21 +310,13 @@ class MiedaDevice(threading.Thread):
manufacturer_code=self._manufacturer_code,
query=query
)
else:
self._parse_cloud_message(status)
elif isinstance(cloud, MeijuCloud):
status = await cloud.get_device_status(
appliance_code=self._device_id,
query=query
)
self._parse_cloud_message(status)
# if self._lua_runtime is not None:
# if query_cmd := self._lua_runtime.build_query(query):
# try:
# await self._build_send(query_cmd)
# return
# except Exception as e:
# traceback.print_exc()
self._parse_cloud_message(status)
def _parse_cloud_message(self, status):
# MideaLogger.debug(f"Received: {decrypted}")
@@ -395,7 +415,13 @@ class MiedaDevice(threading.Thread):
return ParseMessageResult.SUCCESS
async def _send_message(self, data):
if reply := await self._cloud.send_cloud(self._device_id, data):
reply = None
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()):
MideaLogger.debug(f"Decoded: {reply_dec}")
result = self._parse_cloud_message(reply_dec)

View File

@@ -1,4 +1,5 @@
from homeassistant.const import Platform
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import Platform, UnitOfPower, UnitOfElectricPotential
from homeassistant.components.switch import SwitchDeviceClass
DEVICE_MAPPING = {
@@ -6,6 +7,14 @@ DEVICE_MAPPING = {
"rationale": ["off", "on"],
"queries": [{}],
"centralized": [],
"calculate": {
"get": [
{
"lvalue": "[b7_vbattery]",
"rvalue": "[b7_vbatt] / 1000.0"
},
],
},
"entities": {
Platform.SWITCH: {
"power": {
@@ -18,6 +27,25 @@ DEVICE_MAPPING = {
"device_class": SwitchDeviceClass.SWITCH,
}
},
Platform.SENSOR: {
"error_code": {
"device_class": SensorDeviceClass.ENUM
},
"b7_left_status": {
"device_class": SensorDeviceClass.ENUM,
"translation_key": "left_status",
},
"b7_right_status": {
"device_class": SensorDeviceClass.ENUM,
"translation_key": "right_status",
},
"b7_vbattery":{
"device_class": SensorDeviceClass.VOLTAGE,
"unit_of_measurement": UnitOfElectricPotential.VOLT,
"state_class": SensorStateClass.MEASUREMENT,
"translation_key": "battery_voltage",
}
},
Platform.SELECT: {
"wind_pressure": {
"options": {
@@ -28,6 +56,15 @@ DEVICE_MAPPING = {
"extreme": {"wind_pressure": "4"},
}
},
"gear": {
"options": {
"off": {"gear": 0},
"low": {"gear": 1},
"medium": {"gear": 2},
"high": {"gear": 3},
"extreme": {"gear": 4},
}
},
},
}
}

View File

@@ -1,5 +1,4 @@
from homeassistant.const import Platform, UnitOfTemperature, UnitOfTime, PERCENTAGE, PRECISION_HALVES, \
CONCENTRATION_PARTS_PER_MILLION
from homeassistant.const import Platform, UnitOfTemperature, UnitOfTime, PRECISION_WHOLE
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.switch import SwitchDeviceClass
@@ -16,10 +15,6 @@ DEVICE_MAPPING = {
"min": 0,
"max": 3,
"step": 1
},
"cur_temperature": {
"device_class": SensorDeviceClass.TEMPERATURE,
"state_class": SensorStateClass.MEASUREMENT,
}
},
Platform.CLIMATE: {
@@ -32,9 +27,9 @@ DEVICE_MAPPING = {
"target_temperature": "temperature",
"current_temperature": "cur_temperature",
"min_temp": 30,
"max_temp": 75,
"max_temp": 80,
"temperature_unit": UnitOfTemperature.CELSIUS,
"precision": PRECISION_HALVES,
"precision": PRECISION_WHOLE,
}
},
Platform.SWITCH: {
@@ -62,25 +57,8 @@ DEVICE_MAPPING = {
"water_flow": {
"device_class": SwitchDeviceClass.SWITCH,
},
},
Platform.SELECT: {
"func_select": {
"options": {
"low": {"func_select": "low"},
"medium": {"func_select": "medium"}
}
},
"type_select": {
"options": {
"normal": {"type_select": "normal"},
"valve": {"type_select": "valve"},
}
},
"machine": {
"options": {
"real_machine": {"machine": "real_machine"},
"virtual_machine": {"machine": "virtual_machine"}
}
"sterilization": {
"device_class": SwitchDeviceClass.SWITCH,
}
},
Platform.SENSOR: {
@@ -112,22 +90,18 @@ DEVICE_MAPPING = {
"passwater_lowbyte": {
"device_class": SensorDeviceClass.WATER,
"unit_of_measurement": "L",
"state_class": SensorStateClass.MEASUREMENT
"state_class": SensorStateClass.TOTAL
},
"passwater_highbyte": {
"device_class": SensorDeviceClass.WATER,
"unit_of_measurement": "L",
"state_class": SensorStateClass.MEASUREMENT
"state_class": SensorStateClass.TOTAL
},
"rate": {
"device_class": SensorDeviceClass.WATER,
"unit_of_measurement": "L/min",
"state_class": SensorStateClass.MEASUREMENT
"device_class": SensorDeviceClass.ENUM
},
"cur_rate": {
"device_class": SensorDeviceClass.WATER,
"unit_of_measurement": "L/min",
"state_class": SensorStateClass.MEASUREMENT
"device_class": SensorDeviceClass.ENUM
},
"sterilize_left_days": {
"device_class": SensorDeviceClass.DURATION,
@@ -155,19 +129,13 @@ DEVICE_MAPPING = {
"state_class": SensorStateClass.MEASUREMENT
},
"tds_value": {
"device_class": SensorDeviceClass.WATER,
"unit_of_measurement": CONCENTRATION_PARTS_PER_MILLION,
"state_class": SensorStateClass.MEASUREMENT
"device_class": SensorDeviceClass.ENUM
},
"heat_water_level": {
"device_class": SensorDeviceClass.WATER,
"unit_of_measurement": "%",
"state_class": SensorStateClass.MEASUREMENT
"device_class": SensorDeviceClass.ENUM
},
"flow": {
"device_class": SensorDeviceClass.WATER,
"unit_of_measurement": "L/min",
"state_class": SensorStateClass.MEASUREMENT
"device_class": SensorDeviceClass.ENUM
},
"end_time_hour": {
"device_class": SensorDeviceClass.DURATION,
@@ -202,19 +170,17 @@ DEVICE_MAPPING = {
"state_class": SensorStateClass.MEASUREMENT
},
"mg_remain": {
"device_class": SensorDeviceClass.WATER,
"unit_of_measurement": "mg",
"state_class": SensorStateClass.MEASUREMENT
"device_class": SensorDeviceClass.ENUM
},
"waterday_lowbyte": {
"device_class": SensorDeviceClass.WATER,
"unit_of_measurement": "L",
"state_class": SensorStateClass.MEASUREMENT
"state_class": SensorStateClass.TOTAL
},
"waterday_highbyte": {
"device_class": SensorDeviceClass.WATER,
"unit_of_measurement": "L",
"state_class": SensorStateClass.MEASUREMENT
"state_class": SensorStateClass.TOTAL
}
},
Platform.BINARY_SENSOR: {

View File

@@ -15,7 +15,9 @@ DEVICE_MAPPING = {
},
"bubble": {
"device_class": SwitchDeviceClass.SWITCH,
"rationale": [0, 1]
},
"cold_water": {
"device_class": SwitchDeviceClass.SWITCH,
},
"cold_water_master": {
"device_class": SwitchDeviceClass.SWITCH,

View File

@@ -17,9 +17,6 @@ DEVICE_MAPPING = {
"heat": {
"device_class": SwitchDeviceClass.SWITCH,
},
"antifreeze": {
"device_class": SwitchDeviceClass.SWITCH,
},
"lock": {
"device_class": SwitchDeviceClass.SWITCH,
},
@@ -29,22 +26,13 @@ DEVICE_MAPPING = {
"keep_warm": {
"device_class": SwitchDeviceClass.SWITCH,
},
"vacation": {
"device_class": SwitchDeviceClass.SWITCH,
},
"germicidal": {
"device_class": SwitchDeviceClass.SWITCH,
},
"lack_water": {
"device_class": SwitchDeviceClass.SWITCH,
},
"drainage": {
"device_class": SwitchDeviceClass.SWITCH,
},
"wash_enable": {
"device_class": SwitchDeviceClass.SWITCH,
},
"water_way": {
"wash": {
"device_class": SwitchDeviceClass.SWITCH,
},
"soften": {
@@ -65,14 +53,14 @@ DEVICE_MAPPING = {
"cl_sterilization": {
"device_class": SwitchDeviceClass.SWITCH,
},
"holiday_mode": {
"device_class": SwitchDeviceClass.SWITCH,
},
},
Platform.BINARY_SENSOR: {
"heat_status": {
"device_class": BinarySensorDeviceClass.RUNNING,
},
"lack_water": {
"device_class": BinarySensorDeviceClass.PROBLEM,
},
"standby_status": {
"device_class": BinarySensorDeviceClass.RUNNING,
},

View File

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

View File

@@ -1443,6 +1443,9 @@
"left_status": {
"name": "Left Status"
},
"right_status": {
"name": "Right Status"
},
"eq_recipe_action": {
"name": "Eq Recipe Action"
},
@@ -1827,7 +1830,7 @@
"name": "CO2 Check Enable"
},
"cold_water": {
"name": "Cold Water"
"name": "Cold Water Single Loop"
},
"cold_water_ai": {
"name": "Cold Water AI"

View File

@@ -1447,6 +1447,9 @@
"left_status": {
"name": "左侧状态"
},
"right_status": {
"name": "右侧状态"
},
"eq_recipe_action": {
"name": "等量食谱动作"
},
@@ -1831,7 +1834,7 @@
"name": "CO2检测启用"
},
"cold_water": {
"name": "零冷水"
"name": "零冷水单次循环"
},
"cold_water_ai": {
"name": "AI零冷水"