Repository: mill1000/midea-msmart
Pull request #177 (2024-12-05 12:38:32)

Fixed calculation of temperature with decimals
The reported temperature was wrong - decimal part never went ove 0.4.
Im not sure if this is general issue for more devices, or its just me, but I managed to find the culprit in for my Midea AC.

The temperature in payload is rounded to 0.5, but the fraction part is set to full range 0.0-0.9.  When adding it up, it results in incorrect values: 25.5+0.9 = 26.4 instead of 25.9 and in fact it never reaches values 25.5 - 25.9
I need to round the intergral part of temperature to 0.0 to get the correct result.

payload from my AC:
`c001ae647f7f00000000006541120099000000000000000100000000a9`



mill1000: Thanks for the possible solution.

I've review a few of the available references (Lua, JS, other Github projects), and the results seem mixed. A lot of the JS specifically calculates the floating point value, with a few exceptions. The other GH projects mostly use floats as well except for notably https://github.com/midea-lan/midea-local.
mill1000: Either way, to consider merging this change the unit tests need to be updated.
mill1000: Thanks again. This latest version might be viable. I'm still hoping to find another reference source for this.
gaspo: Hey, its still here. Can we re-run the checks, they are already expired, I cant see what was wrong. 
mill1000: I rebased the change onto the latest main branch. This will cause the actions to run, but you'll need to update your local copy
mill1000: You can ignore the unit test failures related to the Cloud, but you have formatting issues and the unit tests for temperature would need to be updated.
gaspo: Hi, Im not sure if I should fix something more to pass the checks. 
mill1000: Thanks again. I switched the parsing to use the code from https://github.com/dudanov/MideaUART here:
https://github.com/dudanov/MideaUART/blob/6cbde3802daf6cf9164478e0584c8e094c0c1396/src/Appliance/AirConditioner/StatusData.cpp#L28-L35

It outputs the same values as your chances, but also covers another corner case.

Patch:
diff --git a/msmart/device/AC/command.py b/msmart/device/AC/command.py
index ced5682..77d4698 100644
--- a/msmart/device/AC/command.py
+++ b/msmart/device/AC/command.py
@@ -860,7 +860,25 @@ class StateResponse(Response):
 
         self._parse(payload)
 
+    def _parse_temperature(self, data: int, decimals: float, fahrenheit: bool) -> Optional[float]:
+        """Parse a temperature value from the payload using additional precision bits as needed."""
+        if data == 0xFF:
+            return None
+
+        # Temperature parsing lifted from https://github.com/dudanov/MideaUART
+        temperature = (data - 50) / 2
+
+        # In Celcius, use additional precision from decimals if present
+        if not fahrenheit and decimals:
+            return int(temperature) + (decimals if temperature >= 0 else -decimals)
+
+        if decimals >= 0.5:
+            return int(temperature) + (0.5 if temperature >= 0 else -0.5)
+
+        return temperature
+
     def _parse(self, payload: memoryview) -> None:
+        """Parse the state response payload."""
 
         self.power_on = bool(payload[1] & 0x1)
         # self.imode_resume = payload[1] & 0x4
@@ -919,12 +937,11 @@ class StateResponse(Response):
         # self.peak_elec = (payload[10] & 0x20) > 0
         # self.natural_fan = (payload[10] & 0x40) > 0
 
-        # Define a local function to decode temperature values
-        def decode_temp(d: int) -> Optional[float]:
-            return ((d - 50)/2 if d != 0xFF else None)
-
-        self.indoor_temperature = decode_temp(payload[11])
-        self.outdoor_temperature = decode_temp(payload[12])
+        # Decode temperatures using additional precision bits
+        self.indoor_temperature = self._parse_temperature(
+            payload[11], (payload[15] & 0xF) / 10, self.fahrenheit)
+        self.outdoor_temperature = self._parse_temperature(
+            payload[12], (payload[15] >> 4) / 10, self.fahrenheit)
 
         # Decode alternate target temperature
         target_temperature_alt = payload[13] & 0x1F
@@ -937,15 +954,6 @@ class StateResponse(Response):
 
         self.display_on = (payload[14] != 0x70)
 
-        # Decode additional temperature resolution
-        if self.indoor_temperature:
-            self.indoor_temperature += (payload[15] & 0xF) / 10
-
-        if self.outdoor_temperature:
-            self.outdoor_temperature += (payload[15] >> 4) / 10
-
-        # TODO Some payloads are shorter than expected. Unsure what, when or why
-        # The lengths below were picked arbitrarily from user payload data
         if len(payload) < 20:
             return
 


Test patch:
diff --git a/msmart/device/AC/test_command.py b/msmart/device/AC/test_command.py
index a4066f3..540580b 100644
--- a/msmart/device/AC/test_command.py
+++ b/msmart/device/AC/test_command.py
@@ -131,12 +131,12 @@ class TestStateResponse(_TestResponseBase):
         # Messages with additional temperature precision bits
         TEST_MESSAGES = {
             # https://github.com/mill1000/midea-msmart/issues/89#issuecomment-1783316836
-            (24.0, 25.1, 10.0): bytes.fromhex(
+            (24.0, 24.6, 9.5): bytes.fromhex(
                 "aa23ac00000000000203c00188647f7f000000000063450c0056190000000000000497c3"),
             # https://github.com/mill1000/midea-msmart/issues/89#issuecomment-1782352164
-            (24.0, 27.0, 10.2): bytes.fromhex(
+            (24.0, 26.5, 9.7): bytes.fromhex(
                 "aa23ac00000000000203c00188647f7f000000000067450c00750000000000000001a3b0"),
-            (24.0, 25.0, 10.0): bytes.fromhex(
+            (24.0, 25.0, 9.5): bytes.fromhex(
                 "aa23ac00000000000203c00188647f7f000080000064450c00501d00000000000001508e"),
         }
 
@@ -162,12 +162,12 @@ class TestStateResponse(_TestResponseBase):
             # Corrected target values from user reported values
             (16.0, 23.2, 18.4): bytes.fromhex("c00181667f7f003c00000060560400420000000000000048"),
             (16.5, 23.4, 18.4): bytes.fromhex("c00191667f7f003c00000060560400440000000000000049"),
-            (17.0, 24.1, 18.3): bytes.fromhex("c00181667f7f003c0000006156050036000000000000004a"),
-            (17.5, 24.3, 18.2): bytes.fromhex("c00191667f7f003c0000006156050028000000000000004b"),
-            (18.0, 24.3, 18.2): bytes.fromhex("c00182667f7f003c0000006156060028000000000000004c"),
-            (18.5, 24.3, 18.2): bytes.fromhex("c00192667f7f003c0000006156060028000000000000004d"),
-            (19.0, 24.3, 18.2): bytes.fromhex("c00183667f7f003c0000006156070028000000000000004e"),
-            (19.5, 24.0, 19.0): bytes.fromhex("c00193667f7f003c00000061570700550000000000000050"),
+            (17.0, 23.6, 18.3): bytes.fromhex("c00181667f7f003c0000006156050036000000000000004a"),
+            (17.5, 23.8, 18.2): bytes.fromhex("c00191667f7f003c0000006156050028000000000000004b"),
+            (18.0, 23.8, 18.2): bytes.fromhex("c00182667f7f003c0000006156060028000000000000004c"),
+            (18.5, 23.8, 18.2): bytes.fromhex("c00192667f7f003c0000006156060028000000000000004d"),
+            (19.0, 23.8, 18.2): bytes.fromhex("c00183667f7f003c0000006156070028000000000000004e"),
+            (19.5, 23.5, 18.5): bytes.fromhex("c00193667f7f003c00000061570700550000000000000050"),
         }
 
         for targets, payload in TEST_RESPONSES.items():