Repository: mill1000/midea-msmart (python)

Issue:
Unable to turn off "breeze" modes on Breezeless device
Originally reported here https://github.com/mill1000/midea-ac-py/issues/212

@lpispek @Jarrekstar Can you try running this example?
```python3
import asyncio
import logging

from msmart.device import AirConditioner as AC

logging.basicConfig(level=logging.DEBUG)

DEVICE_IP = "YOUR_DEVICE_IP"
DEVICE_PORT = 6444
DEVICE_ID = "YOUR_AC_ID"

# For V3 devices
DEVICE_TOKEN = None  # "YOUR_DEVICE_TOKEN"
DEVICE_KEY = None  # "YOUR_DEVICE_KEY"


async def main():
    device = AC(ip=DEVICE_IP, port=6444, device_id=int(DEVICE_ID))
    if DEVICE_TOKEN and DEVICE_KEY:
        await device.authenticate(DEVICE_TOKEN, DEVICE_KEY)

    # Get device capabilities
    await device.get_capabilities()

    # Refresh the state
    print("Refreshing state")
    await device.refresh()

    # Print current breeze properties
    print({
        'breeze_away': device.breeze_away,
        "supports_breeze_away": device.supports_breeze_away,
        'breeze_mild': device.breeze_mild,
        "supports_breeze_mild": device.supports_breeze_mild,
        "breezeless": device.breezeless,
        "supports_breezeless": device.supports_breezeless,

    })

    print("Enabling Breezeless")
    device.breezeless = True
    await device.apply()

    # Print current breeze properties
    print({
        'breeze_away': device.breeze_away,
        'breeze_mild': device.breeze_mild,
        "breezeless": device.breezeless,
    })

    await asyncio.sleep(10)

    # Refresh the state
    print("Refreshing state")
    await device.refresh()

    # Print current breeze properties
    print({
        'breeze_away': device.breeze_away,
        'breeze_mild': device.breeze_mild,
        "breezeless": device.breezeless,
    })

    print("Diabling Breezeless")
    device.breezeless = False
    await device.apply()

    # Print current breeze properties
    print({
        'breeze_away': device.breeze_away,
        'breeze_mild': device.breeze_mild,
        "breezeless": device.breezeless,
    })

    await asyncio.sleep(10)

    # Refresh the state
    print("Refreshing state")
    await device.refresh()

    # Print current breeze properties
    print({
        'breeze_away': device.breeze_away,
        'breeze_mild': device.breeze_mild,
        "breezeless": device.breezeless,
    })


if __name__ == "__main__":
    asyncio.run(main())

```

Pull request description:
Fix inability to turn off breeze modes
It appears devices want a value of 0x01 to disable breeze modes, even though they return a value of 0 to indicate it's off.

Add checks for execution errors in properties responses.

Add/modify test cases for these execution errors

Close #161 



Patch:
diff --git a/msmart/device/AC/command.py b/msmart/device/AC/command.py
index 6be4b09..283b59c 100644
--- a/msmart/device/AC/command.py
+++ b/msmart/device/AC/command.py
@@ -939,7 +939,7 @@ class PropertiesResponse(Response):
                 property = PropertyId(raw_id)
             except ValueError:
                 _LOGGER.warning(
-                    "Unknown property. ID: 0x%04X, Size: %d.", raw_id, size)
+                    "Unknown property ID 0x%04X, Size: %d.", raw_id, size)
                 # Advanced to next property
                 props = props[4+size:]
                 continue
@@ -947,15 +947,23 @@ class PropertiesResponse(Response):
             # Fetch parser for this property
             parser = parsers.get(property, None)
 
-            # Apply parser if it exists
-            if parser is not None:
-                # Parse the property
-                if (value := parser(props[4:])) is not None:
-                    self._properties.update({property: value})
-
-            else:
+            # Check if parser exists
+            if parser is None:
                 _LOGGER.warning(
-                    "Unsupported property. ID: 0x%04X, Size: %d.", property, size)
+                    "Unsupported property %r, Size: %d.", property, size)
+                # Advanced to next property
+                props = props[4+size:]
+                continue
+
+            # Check execution result and log any errors
+            error = props[2] & 0x10
+            if error:
+                _LOGGER.error(
+                    "Property %r failed, Result: 0x%02X.", property, props[2])
+
+            # Parse the property
+            if (value := parser(props[4:])) is not None:
+                self._properties.update({property: value})
 
             # Advanced to next property
             props = props[4+size:]
diff --git a/msmart/device/AC/device.py b/msmart/device/AC/device.py
index 02e7061..1c12155 100644
--- a/msmart/device/AC/device.py
+++ b/msmart/device/AC/device.py
@@ -80,7 +80,7 @@ class AirConditioner(Device):
         PropertyId.BREEZE_AWAY: lambda s: 2 if s._breeze_away else 1,
         PropertyId.BREEZE_CONTROL: lambda s: (4 if s._breezeless else
                                               (3 if s._breeze_mild else
-                                               (2 if s._breeze_away else 0))),
+                                               (2 if s._breeze_away else 1))),
         PropertyId.BREEZELESS: lambda s: s._breezeless,
         PropertyId.RATE_SELECT: lambda s: s._rate_select,
         PropertyId.SWING_LR_ANGLE: lambda s: s._horizontal_swing_angle,


Test patch:
diff --git a/msmart/device/AC/test_command.py b/msmart/device/AC/test_command.py
index 349d640..855cfc9 100644
--- a/msmart/device/AC/test_command.py
+++ b/msmart/device/AC/test_command.py
@@ -654,8 +654,13 @@ class TestPropertiesResponse(_TestResponseBase):
         TEST_RESPONSE = bytes.fromhex(
             "aa18ac00000000000302b0020a0000013209001101000089a4")
 
-        resp = self._test_build_response(TEST_RESPONSE)
-        resp = cast(PropertiesResponse, resp)
+        # Device did not support SWING_UD_ANGLE, check that an error was reported
+        with self.assertLogs("msmart", logging.WARNING) as log:
+            resp = self._test_build_response(TEST_RESPONSE)
+            resp = cast(PropertiesResponse, resp)
+
+            self.assertRegex(
+                log.output[0], "Property .*SWING_UD_ANGLE.* failed, Result: 0x11.")
 
         # Assert response is a correct type
         self.assertEqual(type(resp), PropertiesResponse)
@@ -693,7 +698,7 @@ class TestPropertiesResponse(_TestResponseBase):
             resp = cast(PropertiesResponse, resp)
 
             # Check warning is generated for ID 0x001E
-            self.assertRegex(log.output[0], "Unknown property. ID: 0x001E")
+            self.assertRegex(log.output[0], "Unknown property ID 0x001E")
 
         # Assert response is a correct type
         self.assertEqual(type(resp), PropertiesResponse)
@@ -701,6 +706,22 @@ class TestPropertiesResponse(_TestResponseBase):
         # Assert that the buzzer property is not decoded
         self.assertIsNone(resp.get_property(PropertyId.BUZZER))
 
+    def test_properties_execution_failed(self) -> None:
+        """Test we error when decoding properties that had an execution error."""
+        # https://github.com/mill1000/midea-msmart/issues/161#issuecomment-2282839178
+        TEST_RESPONSE = bytes.fromhex(
+            "aa18ac00000000000302b00243001101041a00000100002ce5")
+
+        with self.assertLogs("msmart", logging.WARNING) as log:
+            resp = self._test_build_response(TEST_RESPONSE)
+            resp = cast(PropertiesResponse, resp)
+
+            self.assertRegex(
+                log.output[0], "Property .*BREEZE_CONTROL.* failed, Result: 0x11.")
+
+        # Assert response is a correct type
+        self.assertEqual(type(resp), PropertiesResponse)
+
 
 class TestResponseConstruct(_TestResponseBase):
     """Test construction of responses from raw data."""
diff --git a/msmart/device/AC/test_device.py b/msmart/device/AC/test_device.py
index cdc5988..bfce7fb 100644
--- a/msmart/device/AC/test_device.py
+++ b/msmart/device/AC/test_device.py
@@ -1,3 +1,4 @@
+import logging
 import unittest
 
 from .command import (EnergyUsageResponse, HumidityResponse,
@@ -175,8 +176,13 @@ class TestUpdateStateFromResponse(unittest.TestCase):
         device.horizontal_swing_angle = AC.SwingAngle.OFF
         device.vertical_swing_angle = AC.SwingAngle.OFF
 
-        resp = Response.construct(TEST_RESPONSE)
-        self.assertIsNotNone(resp)
+        # Device did not support SWING_UD_ANGLE, check that an error was reported
+        with self.assertLogs("msmart", logging.WARNING) as log:
+            resp = Response.construct(TEST_RESPONSE)
+            self.assertIsNotNone(resp)
+
+            self.assertRegex(
+                log.output[0], "Property .*SWING_UD_ANGLE.* failed, Result: 0x11.")
 
         # Assert response is a state response
         self.assertEqual(type(resp), PropertiesResponse)