diff --git a/custom_components/midea_auto_cloud/climate.py b/custom_components/midea_auto_cloud/climate.py index 6a827e7..d4fdfa9 100644 --- a/custom_components/midea_auto_cloud/climate.py +++ b/custom_components/midea_auto_cloud/climate.py @@ -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): diff --git a/custom_components/midea_auto_cloud/core/cloud.py b/custom_components/midea_auto_cloud/core/cloud.py index 99c9a9a..5c5b0de 100644 --- a/custom_components/midea_auto_cloud/core/cloud.py +++ b/custom_components/midea_auto_cloud/core/cloud.py @@ -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 } diff --git a/custom_components/midea_auto_cloud/core/device.py b/custom_components/midea_auto_cloud/core/device.py index c669440..1fa6ba3 100644 --- a/custom_components/midea_auto_cloud/core/device.py +++ b/custom_components/midea_auto_cloud/core/device.py @@ -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: - 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) + elif isinstance(cloud, MeijuCloud): status = await cloud.get_device_status( appliance_code=self._device_id, query=query ) - - self._parse_cloud_message(status) + 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) diff --git a/custom_components/midea_auto_cloud/device_mapping/T0xE2.py b/custom_components/midea_auto_cloud/device_mapping/T0xE2.py index e6046e2..8dc0bb4 100644 --- a/custom_components/midea_auto_cloud/device_mapping/T0xE2.py +++ b/custom_components/midea_auto_cloud/device_mapping/T0xE2.py @@ -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: {