Midea A/C via local XYE

Interesting.

I had just finished testing Emergency Heat - so maybe that was still there.
In which case…

Field 9: 0x80 → Emergency Heat Flag (Bit 7 upper nibble)
Field 9 0x10 → Set Static Pressure (Bit 1 upper nibble)
Field 9 0x05-0x0E → Static Pressure Value (5-15 indicated on the controller)

Here’s my log for C6:

[03:57:55][D][uart_debug:114]: <<< AA:C6:00:00:00:00:00:00:9F:00:04:15:00:39:49:55
[03:57:55][D][uart_debug:114]: <<< AA:C6:00:00:00:00:00:00:9F:00:04:15:00:39:49:55
[03:57:56][D][uart_debug:114]: <<< AA:C6:00:00:00:00:00:00:9F:00:04:15:00:39:49:55
[03:57:56][D][uart_debug:114]: <<< AA:C6:00:00:00:00:00:00:00:30:98:00:00:00:00:60:04:80:CF:BC:D6:28:00:00:07:00:80:80:80:80:FE:55
[03:58:33][D][uart_debug:114]: <<< AA:C6:00:00:00:00:00:00:9E:00:04:15:00:39:4A:55
[03:58:33][D][uart_debug:114]: <<< AA:C6:00:00:00:00:00:00:9E:00:04:15:00:39:4A:55
[03:58:33][D][uart_debug:114]: <<< AA:C6:00:00:00:00:00:00:00:30:98:00:00:00:00:60:04:80:CF:BC:D6:28:00:00:06:00:80:80:80:80:FF:55
[03:58:33][D][uart_debug:114]: <<< AA:C6:00:00:00:00:00:00:00:30:98:00:00:00:00:60:04:80:CF:BC:D6:28:00:00:06:00:80:80:80:80:FF:55
[03:58:49][D][uart_debug:114]: <<< AA:C6:00:00:00:00:00:00:95:00:04:15:00:39:53:55
[03:58:49][D][uart_debug:114]: <<< AA:C6:00:00:00:00:00:00:95:00:04:15:00:39:53:55
[03:58:49][D][uart_debug:114]: <<< AA:C6:00:00:00:00:00:00:95:00:04:15:00:39:53:55
[03:58:49][D][uart_debug:114]: <<< AA:C6:00:00:00:00:00:00:00:30:98:00:00:00:00:60:04:80:CF:BC:D6:28:00:00:05:00:80:80:80:80:00:55
[03:58:50][D][uart_debug:114]: <<< AA:C6:00:00:00:00:00:00:00:30:98:00:00:00:01:60:04:80:CF:BC:D6:28:00:00:FF:00:80:80:80:80:05:55

I think I saw the same thing with respect to setting temp on C or F (it accepts either). However I think the unit won’t remember this after a power off and you have to set it at least once to get the display to change. IIRC that’s why I ended up adding the C4 thing in.

Will test this code when I get a chance!

Interesting. As far as I knew, all of ESPHome used C internally, so I’m not sure how you were testing that. You’d have to have a flag and do the calculation yourself to put it back, as far as I can tell.

I’m doing somre more renaming of things and such now, nothing too additionally crazy LOL.

FIxed the issue I was having. Simply skip processing the result of the 0xC3 command and then process everything moving forward…

Next item to tackle: Contributing — ESPHome - Delays.

I tested it originally by allowing numbers in a wide range on the input. Esphome just treats it like a number, so nothing special.

If you set 70 it will send to the AC 70 and read the same back (AC knows it’s Fahrenheit) l. If you send 25 it reads back 25 (AC knows it’s Celcius).

Issue is that my AC defaults to F and the display starts up that way after a power outage. It needs something to get it back over to C.

This makes sense - the wired controller would start communicating with it as soon as it powered on, so it would send the desired setpoint from the get-go…and it would remember whether you had it set in F or C.

We’ll have to make it a config flag of some sort. :slight_smile:

Might be able to avoid an explicit configuration by looking at the min/max limits for temperature in the component.

I got transmit working in the interim. My current RS485 level shifter doesn’t have hardware flow control so I had to do it by calculating the timing of messages in Python and toggling a GPIO on my FTDI USB dongle :grimacing:. Lucky that the bus is so slow!

Unfortunately I can’t change the static pressure setting while the system is running, this isn’t just a limit enforced by the wall controller.

Set SP1 message:
AA C6 00 00 00 00 00 00 11 00 04 17 00 39 D5 55

Reply when the system was in SP1 / idle:
AA C6 00 00 00 00 00 00 00 30 1C 00 00 00 00 00 10 80 54 BE D6 67 00 00 21 00 00 00 00 00 EE 55

Reply when the system was in SP1 / fan only:
AA C6 00 00 00 00 00 00 00 30 1C 00 00 00 00 00 81 04 5E BE D6 67 00 00 21 00 00 00 00 00 EF 55

Set SP2 message:
AA C6 00 00 00 00 00 00 12 00 04 17 00 39 D4 55

Reply when the system was in SP1 / idle:
AA C6 00 00 00 00 00 00 00 30 1C 00 00 00 00 00 10 80 54 BE D6 66 00 00 22 00 00 00 00 00 EE 55

Reply when system was in SP1 / fan only (failed to change setting):
AA C6 00 00 00 00 00 00 00 30 1C 00 00 00 00 00 81 04 5E BE D6 65 00 00 21 00 00 00 00 00 F1 55

There doesn’t seem to be a field explicitly indicating that the set command was rejected, the only indication seems to be that the static pressure nibble in the reply doesn’t match the request. Per your analysis, the other changing fields correspond to the operating mode or the outdoor temperature.

I will try and get your/exciton’s ESPHome component operating soon and look at extending it to set the static pressure.

But first I am thinking about the control strategy to employ if I have to shut the system down to change the fan speed range. I guess during hot weather if the system is sitting at max compressor speed and still failing to satisfy demand for more than 60 minutes, I will briefly shut it down to “change gears” and significantly increase static pressure/airflow and try to maximise heat transfer with the indoor coil. Otherwise most of the time I can leave it in a quieter & more efficient range.

I’ve now got mdrobnak’s fork of your esphome component working. For now I have it connected in place of the wall controller (CN40 connector) so I can’t tell how the wall controller is responding to things.

For now my automations are still using IR to actually control the system. I notice that when the temperature setpoint is set via IR, the values that are returned to XYE C0 query have an extra bit set such that they are offset by 64C. I have no idea what this bit is meant to indicate.

Initially set 30C cooling mode via XYE, then set the 30C cooling mode via IR, then 29C via IR, finally 29 via XYE. The C0 response changes like:

>>> AA:C0:00:00:00:00:00:00:00:00:00:00:00:3F:01:55
<<< AA:C0:00:00:00:00:30:10:88:04:1E:54:55:55:54:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:04:55
<<< AA:C0:00:00:00:00:30:10:88:04:5E:54:55:55:54:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:C4:55
<<< AA:C0:00:00:00:00:30:10:88:04:5D:54:55:55:54:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:C5:55
<<< AA:C0:00:00:00:00:30:10:88:04:1D:54:55:55:54:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:05:55

0x1E = 30
0x5E = 30 + 64
0x5D = 29 + 64
0x1D = 29

In the middle of that process the XYE ESPHome component showed 94/93C setpoint in Home Assistant :fire: :fire: :fire:

This would complicate detection of the temperature units based on min/max limits, the fahrenheit values corresponding to the standard 17 to 30C range overlap with this 64C offset case.

C F C + 64
17 62.6 81
18 64.4 82
19 66.2 83
20 68 84
21 69.8 85
22 71.6 86
23 73.4 87
24 75.2 88
25 77 89
26 78.8 90
27 80.6 91
28 82.4 92
29 84.2 93
30 86 94

I’m puzzled as to why this sets some different state in the unit depending on whether it came from IR or XYE… is there a different deadband or other control policy depending on whether the system is set via “modern” XYE/CN40 vs. “legacy” IR/5-pin controller? Or just a flag to indicate that “somone else” changed the temperature via another controller / bus?

I have a pretty delicate automation to modulate the compressor speed by spoofing follow me messages, so once I start doing everything via XYE rather than IR I’ll see if the system behaves any differently. I was already anticipating a difference in behaviour as someone mentioned that:

Even in “follow me” mode the unit will include the T1 sensor in the calculation with a 30% weight. Found that in a random video, and can confirm based on behavior of my unit. When the T1 was in the wrong spot and reading 90F when heating it skewed my follow-me temperature way up even though the wall unit was seeing 67F. Once my installer moved the T1 sensor it’s much happier.

From https://github.com/wtahler/esphome-mideaXYE-rs485/tree/main#things-ive-discovered

Worst case I’ll continue using IR to set the operating mode and send follow me while augmenting it with XYE to query operating mode, extra thermistor readings, and set static pressure.

Edit:
I’ve monitored a quick run in both cooling & heating mode. On my system field 20 (byte 0x13) indicates the state of the outdoor unit.
0x00 - Off
0x02 - Fan running
0x03 - Fan and compressor running

In both cooling & heating the fan starts about 5 seconds before the compressor and keeps running for about 30 seconds after it stops.

The behaviour of this field may be interesting during a defrost cycle in heat mode. Something for those of you in the northern hemisphere to monitor?

It rarely gets cold enough here for the defrost to engage. At least after suppressing the completely pointless defrost every 2nd heating cycle (regardless of the ambient temperature!), which my automation suppresses by turning the system ‘off’ in between each heating cycle…

That’s all interesting. Might be able to mask out the offending bit … and I’ll look at what the remote sent that I have when it forced fahrenheit.

Not sure why it would do that.

As far as static pressure, that makes sense, as it has to be off to modify it with the wired controller…

-Matt

I’ve taken your client_id branch and added:

  • A number entity that reports static pressure and allows it to be changed when the system is off.
  • An optional sensor entity for T1 sensor. The current temp of the climate entity is rounded to the nearest 1.0C, I wanted to add this to my HVAC debug dashboard graphs with full precision.

Hooking up the number entity was a pain, I resorted to ‘vibe coding’ with Claude and eventually got things to compile but I suspect there’s some redundant indirection.

I’ve now disabled IR and am doing everything incl. follow me via XYE. So far I haven’t noticed any changes in behaviour (deadband etc).

Very nice! Do note that you can set the precision in ESPHome :slight_smile:

climate:
  - platform: midea_xye
    name: Heatpump
    period: 1s                  # Optional. Defaults to 1s
    timeout: 200ms              # Optional. Defaults to 100ms
    beeper: false               # Optional. Beep on commands.
    visual:                     # Optional. Example of visual settings override.
      min_temperature: 17 °C    # min: 17
      max_temperature: 30 °C    # max: 30
      temperature_step: 0.5 °C  # min: 0.5

I think I have an idea of how to convert this into a state machine-ish to get rid of the warnings about the components taking too long to run. The changes for that are on the “delays” branch of my repo. None of that is tested at the moment as I’m traveling at the second. I’ll be home tomorrow night and will test it a bit then.

-Matt
So that should fix that particular issue.

Yes I haven’t added in anything new except for the outdoor temp sensor as the python->C stuff is a little bit of a pain…

As we discover more which we can set up via XYE, I’ll definitely try to add in stuff. I’ll look at what you have and merge it in to what I have for the static pressure stuff.

I just pushed an update to client_id that includes support for sending / receiving values in Fahrenheit instead of Celsius. Currently using this here at my house now. As was suggested, I should probalby add in some range detection to detect what it is currently set to…but that’s a next week problem LOL.

To get the correct values from the AC, this means the read-back of the target / set temperature is from C4 instead of C0 now.

I’ll have to re-test setting stuff with the controller to see how to move it from C to F or vice versa. At this tiem…I can’t see how to do that. Wired controlle rodes its own thing no matter what’s coming in.

-Matt

My automated static pressure “gear change” worked, with caveats. The number entity is “optimistic” such that changes immediately effect the number entity in Home Assistant, but if the setting wasn’t accepted for whatever reason it takes ~2s for the true value from C4 response to be reflected back in Home Assistant to indicate failure.

I’m not sure if this behaviour is fundamental to the number entity, in which case I can only think to split it into a read-only sensor & a separate service to make changes. Then I can be confident that the sensor never shows tentative values and I can get rid of the delicate sleep() statements in my automation.

Oh and I’ll add boolean sensors to reflect outdoor fan & compressor state bits that my system exposes and/or use them to indicate cool vs. cooling like how you’ve done heat vs. heating.

When the temp is set via IR I see the same values with the extra bit (effective 64C offset) in both byte 10 of C0 and byte 18 of C4. Maybe this quirk only affects Celsius in which case it can safely be applied if the right mode is set in the YAML.

Ah hah

mdrobnak@cnc:~/vcs/git/esphome/esphome/components/midea_xye$ git diff 786998929cee75e716361c533e335ac6db39d8cb -w
diff --git a/esphome/components/midea_xye/air_conditioner.cpp b/esphome/components/midea_xye/air_conditioner.cpp
index d3db1a60..f907a3c3 100644
--- a/esphome/components/midea_xye/air_conditioner.cpp
+++ b/esphome/components/midea_xye/air_conditioner.cpp
@@ -258,6 +258,10 @@ void AirConditioner::ParseResponse(uint8_t cmdSent) {
             break;
         }
 
+        if ((RXData[RX_C0_BYTE_OP_MODE] & 0x10) == 0x10) {
+          mode = ClimateMode::CLIMATE_MODE_HEAT_COOL;
+        }
+
         uint8_t current_fan_speed = RXData[RX_C0_BYTE_FAN_MODE] & 0x0F;
         switch (current_fan_speed) {
           case FAN_MODE_HIGH:
@@ -308,6 +312,23 @@ void AirConditioner::ParseResponse(uint8_t cmdSent) {
           need_publish = true;
         }
 
+        if ((this->mode == climate::CLIMATE_MODE_HEAT_COOL) &&
+            (RXData[RX_C0_BYTE_OP_MODE] == OP_MODE_AUTO_COOLING) &&
+            (this->action != climate::CLIMATE_ACTION_COOLING)) {
+          this->action = climate::CLIMATE_ACTION_COOLING;
+          need_publish = true;
+        } else if ((this->mode == climate::CLIMATE_MODE_HEAT_COOL) &&
+                   (RXData[RX_C0_BYTE_OP_MODE] == 0x91) &&
+                   (this->action != climate::CLIMATE_ACTION_FAN)) {
+          this->action = climate::CLIMATE_ACTION_FAN;
+          need_publish = true;
+        } else if ((this->mode == climate::CLIMATE_MODE_HEAT_COOL) &&
+                   (RXData[RX_C0_BYTE_OP_MODE] == 0x94) &&
+                   (this->action != climate::CLIMATE_ACTION_HEATING)) {
+          this->action = climate::CLIMATE_ACTION_HEATING;
+          need_publish = true;
+        }
+
         if ((this->swing_mode != ClimateSwingMode::CLIMATE_SWING_OFF) !=
             (bool)(RXData[RX_C0_BYTE_MODE_FLAGS] & MODE_FLAG_SWING))
           need_publish = true;
diff --git a/esphome/components/midea_xye/air_conditioner.h b/esphome/components/midea_xye/air_conditioner.h
index 42aded8e..408b0a7e 100644
--- a/esphome/components/midea_xye/air_conditioner.h
+++ b/esphome/components/midea_xye/air_conditioner.h
@@ -29,6 +29,7 @@
 #define OP_MODE_DRY 0x82
 #define OP_MODE_HEAT 0x84
 #define OP_MODE_COOL 0x88
+#define OP_MODE_AUTO_COOLING 0x98
 
 #define FAN_MODE_AUTO 0x80
 #define FAN_MODE_OFF 0x00

:slight_smile: - Fan, Cooling, and Heating are verified.

FYI client_id branch is a bit…odd at the moment. Need to log things on initial start a bit better.
-Matt

Updated client_id branch.

  • Handle Fahrenheit items much better now. Updated the ForceRead var to be in the C4 response parsing, as that needs to be able to set the target tremp before claming all was OK. I had some NaN values and other oddities otherwise.
  • Handle Heat/Cool mode correctly. I can tell when the system is actively heating or cooling via the data returned from the unit. Reliably set Off/Idle.

So this is relatively stable now lol.
Now about to break the entire polling mechanism to get rid of the arnings about stuff taking too long lol.

Need to re-test Celsius mode at some point too.

-Matt

Edit: I re-worked things on the delays branch, and it actually works on the first test…I’m kinda suprised haha. But component time went from 400 ms to 50. :slight_smile:

Ok. I’m pretty happy so far with the new code.
I probably should add some more debug logging just in case, but as of the moment, things are working well.

At this point, as per the README, current sensor and swing mode are things I can’t test as they just aren’t applicable to the Air Handler.

In the “Not yet implemented” section:

  • Setting timers direct to unit. No real need since automations can do this Not going to work on this at this time.
  • Figure out how to force display to C or F. Setting temp in C doesn’t force display to C. Also unsure how this works
  • Freeze protection My manual says this is only available by remote…but obviously it’s forwarded…I’ll test this when I have some time
  • Silent mode Not sure if supported by the AHU
  • Lock/Unlock Will test this when I have time

aside from these things I’m pretty happy with this right now. The only thing that would be useful at some point would be to make the default value of C vs F dependent on your region settings in Home Assistant.

I’ll see about pulling in @rymo’s commits for the static pressure stuff.

-Matt

I created a branch called custom_auto that I’m trying to add in a two-point (high temp / low temp) temperature setting that’ll cycle through the modes in a less insane algorithm than the stock one. :slight_smile:

I pushed the code. Totally untested. It does compile lol.

It’s been a crazy weekend, I’m exhausted, so I’m going to bed early.

-Matt

Ok…

First - didn’t realize there was a .clang-format file, so clang-format -i *.cpp *.h was all that’s needed…

Second - I merged the static pressure changes into client_id and delays branches. Delays branch was up for 7 days with no issue so I’d just use that.

Working on the custom_auto stuff soon.

For me, offsetting the desired temp ie 70 degrees F, to 73 degrees F, has pretty much the whole house at 70. So I’m pretty happy at that. For me, I need to run the fan on High for it to make sure the air circulates. I probbly need to tune the static pressure setting myself…

Edit: custom_auto is very very broken.

-Matt

I finally got a chance to wire directly into the CCM/XYE terminals in the indoor unit. Physically I was successful, but bad news is that there seem to be some differences between responses and supported message types between the CCM/XYE interface and CN40 (which I had been calling “XYE” until now in this thread as they appeared to be the same).

QUERY   >>> AA:C0:00:00:00:00:00:00:00:00:00:00:00:3F:01:55
CN40    <<< AA:C0:00:00:00:00:30:10:00:04:14:5D:59:59:58:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:81:55
CCM/XYE <<< AA:C0:00:00:00:00:30:10:00:04:14:5D:59:FF:58:FF:00:00:00:00:00:00:00:00:00:00:00:14:FF:FF:CA:55
  • Byte 13: T2B indoor coil outlet temperature. This thermistor isn’t connected in my system. Via CN40 the value of T2 was also reported as T2B (I had noticed that they were always the same), but CCM/XYE indicates that it is absent.
  • Byte 15: Current, CCM/XYE always 0xFF rather than CN40 always 0x00. Seems similar to thermistor, via CCM/XYE 0xFF is used to indicate that a sensor is absent wheras via CN40 plausible placeholder values are used instead so that wall controllers don’t need to be programmed for the supported feature set?
  • Bytes 27-29: ???
QUERY   >>> AA:C4:00:00:00:00:00:00:00:00:00:00:00:3B:01:55
CN40    <<< AA:C4:00:00:00:00:00:00:00:30:1C:00:00:00:00:00:01:04:14:91:9E:57:00:00:22:00:00:00:00:00:2F:55
CCM/XYE <<< AA:C5:00:00:00:00:00:00:00:00:00:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00:00:FF:00:00:44:55

C4 “extended query” command not supported via XYE, C5 is an error response?

I first noticed something suspect when I saw the indoor coil outlet temp was 107.5C, I got spooked that I’d knocked the thermistor connector out and that I’d have to climb back to access the unit!

At least now I have both buses easily accessible so I can help with compatibility testing if we try to submit the ESPHome component and make it handle the CCM/XYE subset. I’m feeling too defeated to look at the code today.

Hardware wise I have received an M5 Stack Atom Lite & “Tail485” which is perfect to replace the wall controller, so at least I can make it look tidier.

This is fascinating - I’m using the “RS485” labeled connector on mine, which I thought others have verified re wired the same as XYE. That said, I an connecting through the Wifi adapter thing. (Edit: So this should be the CN40 connector)

Are you saying on your unit you have two isolated buses? I don’t have anything labelled CCM either.

I also do not have T2B. It’s 0s for me, so the wall controller inquiry diagostic says like -19C at all times.

Do you have any pictures of these connection points?

Yay for variations in control strategies. :frowning: