ESP Haier: Haier Air Conditioner + ESP Home + Wemos D1 mini

Hi, is here someone with as50s2f1fa aircon with the new wifi module (black box, hON app, version 4? ) and can provide serial number? I want to try new haier module with old aircon, but I need correct serial number for that.

I managed to get as35s2f1fa-wh working with the new module instead of the old one with usb connector (that is not using actual usb), but for that i had also the new unit with correct SN to identify it.

@Nikola_Jovanoski Can you please send me your SN? Maybe even AC50S2SG1FA would work for, I just need to try in the app.

Update: nevermind, I don’t think anymore it is possible to use newer module with as50s2f1fa, it works with as35s2f1fa thought, maybe it would be possible using the “jumpers” on the board, but i don’t want to mess with that.

I changed from internet provider yesterday and now my AC units doesn’t work anymore. The ESP’s are online and I can see the logfiles. I don’t know the solution. Can someone help me in the right direction?

Maybe you new router block the ESP messages or change the IP for device?

Try remove the esphome integration and you add it again.

1 Like

I did it yesterday and the problem is solved!
Thanks anyway.

Hello,
I had some issues with this, such as changes through home assistant not respecting the current display or purify/health state when there is a change, so i went ahead and modified it to work for me, also cleaned up a few things that didnt seem to be working. Biggest change was, instead of setting each bit individually and trying to remember them all, we use the last received AC status and only modify the bits we need. This fixed purify mode for me and the display, and in my quick testing everything seems to work with this new code. This may fix other issues as well since we just basically echo back the control bits the ac tells us except for the bits we changed to control instead of trying to know what every bit does.

Next big question is, how do I add options for the items not supported by “climate” entities in home assistant in the yaml? Some switches and status for health/purify mode, display on/off, etc would be nice to have in home assistant. We already have the functions to get the status of those Items, but whats the best way to add entities for those in the yaml?

Here is my attached HaierV4.h, once i figure out how to implement custom switches for missing features in the yaml file, adding the missing features such as turbo mode, quiet mode, timers, etc should be easy since we already know how to get them from the AC unit.

/**
* Create by Miguel Ángel López on 20/07/19
* Modified by Alba Prades on 21/07/20
* Modified by Alba Prades on 13/08/20 
* Modified by Alba Prades on 25/08/20: Added fan, dry and swing features
*      Added modes
* Modified by Carlo Pinasco on 25/08/21: Workaround to fix Purify problem and fix deprecated warning messages.
* Modified By IvoH on 1/31/22, read status from AC for every control command, cleanup not working options. 
**/
#define DEBUG

#ifndef HAIER_ESP_HAIER_H
#define HAIER_ESP_HAIER_H

#include "esphome.h"
#include <string>

using namespace esphome;
using namespace esphome::climate;

// Updated read offset

#define MODE_OFFSET 			14
#define MODE_MSK				0xF0
	#define MODE_AUTO       	0x00
	#define MODE_DRY			0x40
	#define MODE_COOL			0x20
	#define MODE_HEAT			0x80
	#define MODE_FAN			0xC0
#define FAN_MSK					0x0F
	#define FAN_LOW	    		0x03
	#define FAN_MID		  		0x02
	#define FAN_HIGH	     	0x01
	#define FAN_AUTO	   		0x05
	
#define HORIZONTAL_SWING_OFFSET		19
	#define HORIZONTAL_SWING_CENTER 		0x00
	#define HORIZONTAL_SWING_MAX_LEFT 		0x03
	#define HORIZONTAL_SWING_LEFT 			0x04
	#define HORIZONTAL_SWING_MAX_RIGHT 		0x06
	#define HORIZONTAL_SWING_RIGHT 			0x05
	#define HORIZONTAL_SWING_AUTO 			0x07
	
#define VERTICAL_SWING_OFFSET			13
	#define VERTICAL_SWING_MAX_UP			0x02
	#define VERTICAL_SWING_UP				0x04
	#define VERTICAL_SWING_CENTER				0x06
	#define VERTICAL_SWING_DOWN				0x08
	#define VERTICAL_SWING_HEALTH_UP			0x01
	#define VERTICAL_SWING_HEALTH_DOWN		0x03
	#define VERTICAL_SWING_AUTO 				0x0C

#define TEMPERATURE_OFFSET   	22

#define STATUS_DATA_OFFSET			17 // Purify/Quiet mode/OnOff/...
	#define POWER_BIT				(0)	
	#define PURIFY_BIT_STATUS				(1)	
	#define QUIET_BIT				(3)	
	#define AUTO_FAN_MAX_BIT		(4)

#define PURIFY_DATA_OFFSET 21 //multiple bits change when purify is enabled, some are here
	#define PURIFY_BIT_CONTROL_1	(2)
	#define PURIFY_BIT_CONTROL_2	(3)

#define DISPLAY_STATUS_OFFSET 16 //display status is here
	#define DISPLAY_BIT				(1)	//if the display is on or off

#define SET_POINT_OFFSET 		12	

// Another byte
	#define SWING        		27
	#define SWING_OFF          	0
	#define SWING_VERTICAL     	1
	#define SWING_HORIZONTAL   	2
	#define SWING_BOTH

	#define LOCK        		28
	#define LOCK_ON     		80
	#define LOCK_OFF    		00

// Updated read offset


#define COMMAND_OFFSET			9
#define RESPONSE_POLL			2
#define CONTROL_COMMAND_OFFSET  11 //where our commands start in the control message

#define CRC_OFFSET(message)		(2 + message[2])

// Control commands
#define CTR_POWER_OFFSET		13
#define CTR_POWER_ON		0x01
#define CTR_POWER_OFF		0x00
	
#define POLY 0xa001


// temperatures supported by AC system
#define MIN_SET_TEMPERATURE 16
#define MAX_SET_TEMPERATURE 30

//if internal temperature is outside of those boundaries, message will be discarded
#define MIN_VALID_INTERNAL_TEMP 10
#define MAX_VALID_INTERNAL_TEMP 50

class Haier : public Climate, public PollingComponent {

private:

    byte lastCRC;
    byte status[47];
	
	byte initialization_1[13] = {0xFF,0xFF,0x0A,0x0,0x0,0x0,0x0,0x0,0x00,0x61,0x00,0x07,0x72};
	byte initialization_2[13] = {0xFF,0xFF,0x08,0x40,0x0,0x0,0x0,0x0,0x0,0x70,0xB8,0x86,0x41};
 	byte poll[15] = {0xFF,0xFF,0x0A,0x40,0x00,0x00,0x00,0x00,0x00,0x01,0x4D,0x01,0x99,0xB3,0xB4};
	byte control_command[25] = {0xFF,0xFF,0x14,0x40,0x00,0x00,0x00,0x00,0x00,0x01,0x60};
	
	bool first_status_received = false;
	byte climate_mode_fan_speed = FAN_AUTO;
	byte fan_mode_fan_speed = FAN_HIGH;

	// Some vars for debuging purposes	
	byte previous_status[47];
	bool previous_status_init = false;
	
	
	// Functions

	void SetHvacModeControl(byte mode)
	{
		control_command[MODE_OFFSET] &= ~MODE_MSK;
		control_command[MODE_OFFSET] |= mode;
	}	
	
	byte GetHvacModeStatus()
	{
		return status[MODE_OFFSET] & MODE_MSK;
	}	
	
	void SetTemperatureSetpointControl(byte temp)
	{ 
		control_command[SET_POINT_OFFSET] = temp;
	}	
	
	byte GetTemperatureSetpointStatus()
	{
		return status[SET_POINT_OFFSET];
	}	
	
	void SetFanSpeedControl(byte fan_mode)
	{
		control_command[MODE_OFFSET] &= ~FAN_MSK;
		control_command[MODE_OFFSET] |= fan_mode;
	}
	
	byte GetFanSpeedStatus()
	{
		return status[MODE_OFFSET] & FAN_MSK;
	}		
	
	void SetHorizontalSwingControl(byte swing_mode)
	{
		control_command[HORIZONTAL_SWING_OFFSET] = swing_mode;
	}
	
	byte GetHorizontalSwingStatus()
	{
		return status[HORIZONTAL_SWING_OFFSET];
	}	
	
	void SetVerticalSwingControl(byte swing_mode)
	{
		control_command[VERTICAL_SWING_OFFSET] = swing_mode;
	}
	
	byte GetVerticalSwingStatus()
	{
		return status[VERTICAL_SWING_OFFSET];
	}	
	
	void SetQuietModeControl(bool quiet_mode)
	{
		byte tmp;
		byte msk;
		
		msk = (0x01 << QUIET_BIT);		
		
		if(quiet_mode == true){
			control_command[STATUS_DATA_OFFSET] |= msk;
		}
		else{
			msk = ~msk;
			control_command[STATUS_DATA_OFFSET] &= msk;
		}
	}
	
	bool GetQuietModeStatus( void )
	{
		bool ret = false;		
		byte tmp;
		byte msk;
		
		msk = (0x01 << QUIET_BIT);
		tmp = status[STATUS_DATA_OFFSET] & msk;
		
		if(tmp != 0) ret = true;
		
		return ret;
	}
	
	
void SetPurifyControl(bool purify_mode)
	{
		byte tmp;
		byte msk;
		
		msk = (0x01 << PURIFY_BIT_STATUS);		
		
		if(purify_mode == true){
			control_command[STATUS_DATA_OFFSET] |= msk;
			msk = (0x01 << PURIFY_BIT_CONTROL_1);	
			control_command[PURIFY_DATA_OFFSET] |= msk;
			msk = (0x01 << PURIFY_BIT_CONTROL_2);	
			control_command[PURIFY_DATA_OFFSET] |= msk;
		}
		else{
			msk = ~msk;
			control_command[STATUS_DATA_OFFSET] &= msk;

			msk = (0x01 << PURIFY_BIT_CONTROL_1);
			msk = ~msk;
			control_command[PURIFY_DATA_OFFSET] &= msk;
			msk = (0x01 << PURIFY_BIT_CONTROL_2);
			msk = ~msk;
			control_command[PURIFY_DATA_OFFSET] &= msk;
		}
	}
	bool GetPurifyStatus( void )
	{
		bool ret = false;		
		byte tmp;
		byte msk;
		
		msk = (0x01 << PURIFY_BIT_STATUS);
		tmp = status[STATUS_DATA_OFFSET] & msk;
		
		if(tmp != 0) ret = true;
		
		return ret;
	}
	
		void SetDisplayStatus(bool display_on)
	{
		byte tmp;
		byte msk;
		
		msk = (0x01 << DISPLAY_BIT);		
		
		if(display_on == true){
			control_command[DISPLAY_STATUS_OFFSET] |= msk;
		}
		else{
			msk = ~msk;
			control_command[DISPLAY_STATUS_OFFSET] &= msk;
		}
	}

	bool GetDisplayStatus( void )
	{
		bool ret = false;		
		byte tmp;
		byte msk;
		
		msk = (0x01 << DISPLAY_BIT);
		tmp = status[DISPLAY_STATUS_OFFSET] & msk;
		
		if(tmp != 0) ret = true;
		
		return ret;
	}
	
	
	void SetPowerControl(bool power_mode)
	{
		byte tmp;
		byte msk;
		
		msk = (0x01 << POWER_BIT);		
		
		if(power_mode == true){
			control_command[STATUS_DATA_OFFSET] |= msk;
		}
		else{
			msk = ~msk;
			control_command[STATUS_DATA_OFFSET] &= msk;
		}
	}
	
	bool GetPowerStatus( void )
	{
		bool ret = false;		
		byte tmp;
		byte msk;
		
		msk = (0x01 << POWER_BIT);
		tmp = status[STATUS_DATA_OFFSET] & msk;
		
		if(tmp != 0) ret = true;
		
		return ret;
	}
	
	
	bool GetFastModeStatus( void )
	{
		bool ret = false;		
		byte tmp;
		byte msk;
		
		msk = (0x01 << AUTO_FAN_MAX_BIT);
		tmp = status[STATUS_DATA_OFFSET] & msk;
		
		if(tmp != 0) ret = true;
		
		return ret;
	}
	
	void SetFastModeControl(bool fast_mode)
	{
		byte tmp;
		byte msk;
		
		msk = (0x01 << AUTO_FAN_MAX_BIT);		
		
		if(fast_mode == true){
			control_command[STATUS_DATA_OFFSET] |= msk;
		}
		else{
			msk = ~msk;
			control_command[STATUS_DATA_OFFSET] &= msk;
		}
	}
	
	
	void CompareStatusByte()
	{
		int i;
		
		if(previous_status_init == false){
			for (i=0;i<sizeof(status);i++){
				previous_status[i] = status[i];
			}
			previous_status_init = true;
		}
		
		for (i=0;i<sizeof(status);i++)
		{
			if(status[i] != previous_status[i]){
				ESP_LOGD("Debug", "Status byte %d: 0x%X --> 0x%X ", i, previous_status[i],status[i]);
			}
			previous_status[i] = status[i];
		}
	}


public:

    Haier() : PollingComponent(5 * 1000) {
        lastCRC = 0;
    }


    
    void setup() override {
        
        Serial.begin(9600);
		Serial.write(initialization_1, sizeof(initialization_1));
        auto raw = getHex(initialization_1, sizeof(initialization_1));
        ESP_LOGD("Haier", "initialization_1: %s ", raw.c_str());
		Serial.write(initialization_2, sizeof(initialization_2));
        raw = getHex(initialization_2, sizeof(initialization_2));
        ESP_LOGD("Haier", "initialization_2: %s ", raw.c_str());
    }

    void loop() override  {
		byte data[47];
        if (Serial.available() > 0) {
			if (Serial.read() != 255) return;
			if (Serial.read() != 255) return;
			
			data[0] = 255;
			data[1] = 255;

            Serial.readBytes(data+2, sizeof(data)-2);
			
			// If is a status response
			if (data[COMMAND_OFFSET] == RESPONSE_POLL) {
				// Update the status frame
				memcpy(status, data, sizeof(status));
				parseStatus();
			}
		}
    }

    void update() override {
        
        Serial.write(poll, sizeof(poll));
        auto raw = getHex(poll, sizeof(poll));
        ESP_LOGD("Haier", "POLL: %s ", raw.c_str());
    }

protected:
    ClimateTraits traits() override {
        auto traits = climate::ClimateTraits();
        traits.set_supported_modes(
        {
            climate::CLIMATE_MODE_OFF,
            climate::CLIMATE_MODE_COOL,
            climate::CLIMATE_MODE_HEAT,
            climate::CLIMATE_MODE_FAN_ONLY,
            climate::CLIMATE_MODE_DRY,
            climate::CLIMATE_MODE_AUTO
        });

        traits.set_supported_fan_modes(
        {
            climate::CLIMATE_FAN_AUTO,
            climate::CLIMATE_FAN_LOW,
            climate::CLIMATE_FAN_MEDIUM,
            climate::CLIMATE_FAN_HIGH,
        });

        traits.set_supported_swing_modes(
        {
            climate::CLIMATE_SWING_OFF,
            climate::CLIMATE_SWING_BOTH,
            climate::CLIMATE_SWING_VERTICAL,
            climate::CLIMATE_SWING_HORIZONTAL
        });

        traits.set_visual_min_temperature(MIN_SET_TEMPERATURE);
        traits.set_visual_max_temperature(MAX_SET_TEMPERATURE);
        traits.set_visual_temperature_step(1.0f);
        traits.set_supports_current_temperature(true);
        return traits;
    }

public:

    void parseStatus() {


        auto raw = getHex(status, sizeof(status));
        ESP_LOGD("Haier", "Readed message ALBA: %s ", raw.c_str());

        byte check = getChecksum(status, sizeof(status));

        if (check != status[CRC_OFFSET(status)]) {
            ESP_LOGW("Haier", "Invalid checksum (%d vs %d)", check, status[CRC_OFFSET(status)]);
            return;
        }

        lastCRC = check;
		//update all statuses in the control part of the message from what we recieve from the status message
		
		for (int i=CONTROL_COMMAND_OFFSET;i<(sizeof(control_command)-3);i++)
		{
			control_command[i] = status[i];
		}

		if(GetHvacModeStatus() == MODE_FAN){
			fan_mode_fan_speed = GetFanSpeedStatus();
		}
		else{
			climate_mode_fan_speed = GetFanSpeedStatus();
		}

        current_temperature = status[TEMPERATURE_OFFSET]/2;
        target_temperature = status[SET_POINT_OFFSET] + 16;

        if(current_temperature < MIN_VALID_INTERNAL_TEMP || current_temperature > MAX_VALID_INTERNAL_TEMP 
            || target_temperature < MIN_SET_TEMPERATURE || target_temperature > MAX_SET_TEMPERATURE){
            ESP_LOGW("Haier", "Invalid temperatures");
            return;
        }

		first_status_received = true;

		#ifdef DEBUG
		ESP_LOGD("Debug", "HVAC Mode = 0x%X", GetHvacModeStatus());
		ESP_LOGD("Debug", "Display status = 0x%X", GetDisplayStatus());
		ESP_LOGD("Debug", "Power Status = 0x%X", GetPowerStatus());
		ESP_LOGD("Debug", "Purify status = 0x%X", GetPurifyStatus());
		ESP_LOGD("Debug", "Quiet mode Status = 0x%X", GetQuietModeStatus());
		ESP_LOGD("Debug", "Fast mode Status = 0x%X", GetFastModeStatus());
		ESP_LOGD("Debug", "Fan speed Status = 0x%X", GetFanSpeedStatus());
		ESP_LOGD("Debug", "Horizontal Swing Status = 0x%X", GetHorizontalSwingStatus());
		ESP_LOGD("Debug", "Vertical Swing Status = 0x%X", GetVerticalSwingStatus());
		ESP_LOGD("Debug", "Set Point Status = 0x%X", GetTemperatureSetpointStatus());
		#endif

		CompareStatusByte();
		
		
		// Update home assistant component
		
        if (GetPowerStatus() == false) {
            mode = CLIMATE_MODE_OFF;
		} else {
			// Check current hvac mode
            switch (GetHvacModeStatus()) {
                case MODE_COOL:
                    mode = CLIMATE_MODE_COOL;
                    break;
                case MODE_HEAT:
                    mode = CLIMATE_MODE_HEAT;
                    break;
                case MODE_DRY:
				    mode = CLIMATE_MODE_DRY;
					break;
				case MODE_FAN:
                    mode = CLIMATE_MODE_FAN_ONLY;
                    break;
                case MODE_AUTO:
                default:
                    mode = CLIMATE_MODE_AUTO;
            }
					
			// Get fan speed
			// If "quiet mode" is set we will read it as "fan low"
			if ( GetQuietModeStatus() == true) {
                fan_mode = CLIMATE_FAN_LOW;
            }
			// If we detect that fast mode is on the we read it as "fan high"
			else if( GetFastModeStatus() == true) {
				fan_mode = CLIMATE_FAN_HIGH;
			}			
			else {				
				// No quiet or fast so we read the actual fan speed.
                switch (GetFanSpeedStatus()) {
                    case FAN_AUTO:
                        fan_mode = CLIMATE_FAN_AUTO;
                        break;
                    case FAN_MID:
                        fan_mode = CLIMATE_FAN_MEDIUM;
                        break;
					case FAN_LOW:
						fan_mode = CLIMATE_FAN_LOW;
                        break;
                    case FAN_HIGH:
                        fan_mode = CLIMATE_FAN_HIGH;
                        break;
                    default:
                        fan_mode = CLIMATE_FAN_AUTO;
						
                }
            }				


			// Check the status of the swings (vertical and horizontal and translate according component configuration
			if( (GetHorizontalSwingStatus() == HORIZONTAL_SWING_AUTO) && (GetVerticalSwingStatus() == VERTICAL_SWING_AUTO) ){
				swing_mode = CLIMATE_SWING_BOTH;				
			}
			else if(GetHorizontalSwingStatus() == HORIZONTAL_SWING_AUTO){
				swing_mode = CLIMATE_SWING_HORIZONTAL;
			}
			else if(GetVerticalSwingStatus() == VERTICAL_SWING_AUTO){
				swing_mode = CLIMATE_SWING_VERTICAL;
			}
			else{
				swing_mode = CLIMATE_SWING_OFF;
			}
		}

        this->publish_state();

    }


    void control(const ClimateCall &call) override {
		
        ClimateMode new_mode;
		bool new_control_cmd = false;
		
		
		ESP_LOGD("Control", "Control call");
		
		if(first_status_received == false){
			ESP_LOGD("Control", "No action, first poll answer not received");
			return;
		}

        if (call.get_mode().has_value()) {
            // User requested mode change
            new_mode = *call.get_mode();
        
			ESP_LOGD("Control", "*call.get_mode() = %d", new_mode);
			
            switch (new_mode) {
                case CLIMATE_MODE_OFF:
					SetPowerControl(false);
					sendData(control_command, sizeof(control_command)); 
                    break;
					
                case CLIMATE_MODE_AUTO:
					SetPowerControl(true);
					SetHvacModeControl(MODE_AUTO);
					SetFanSpeedControl(climate_mode_fan_speed);	
					sendData(control_command, sizeof(control_command));
                    break;
					
                case CLIMATE_MODE_HEAT:	
					SetPowerControl(true);
					SetHvacModeControl(MODE_HEAT);
					SetFanSpeedControl(climate_mode_fan_speed);	
					sendData(control_command, sizeof(control_command));
                    break;
					
                case CLIMATE_MODE_DRY:		
					SetPowerControl(true);
					SetHvacModeControl(MODE_DRY);
					SetFanSpeedControl(climate_mode_fan_speed);				
					sendData(control_command, sizeof(control_command));					
                    break;
					
                case CLIMATE_MODE_FAN_ONLY:				
					SetPowerControl(true);
					SetHvacModeControl(MODE_FAN);
					SetFanSpeedControl(fan_mode_fan_speed); //pick a speed, auto doesnt work in fan only mode				
					sendData(control_command, sizeof(control_command));
                    break;

                case CLIMATE_MODE_COOL:
					SetPowerControl(true);
					SetHvacModeControl(MODE_COOL);
					SetFanSpeedControl(climate_mode_fan_speed);	
					sendData(control_command, sizeof(control_command));
                    break;
            }
            // Publish updated state
            mode = new_mode;
            this->publish_state();
		}
		
		        //Set fan speed
        if (call.get_fan_mode().has_value()) {
            switch(call.get_fan_mode().value()) {
                case CLIMATE_FAN_LOW:
					SetFanSpeedControl(FAN_LOW);
                    break;
                case CLIMATE_FAN_MEDIUM:
					SetFanSpeedControl(FAN_MID);
                    break;
                case CLIMATE_FAN_HIGH:
					SetFanSpeedControl(FAN_HIGH);
                    break;
                case CLIMATE_FAN_AUTO:
                    SetFanSpeedControl(FAN_AUTO);
                    break;
			}
			sendData(control_command, sizeof(control_command)); 
		}

        //Set swing mode
        if (call.get_swing_mode().has_value()){
            switch(call.get_swing_mode().value()) {
                case CLIMATE_SWING_OFF:
					// When not auto we decide to set it to the center
					SetHorizontalSwingControl(HORIZONTAL_SWING_CENTER);
					// When not auto we decide to set it to the center
					SetVerticalSwingControl(VERTICAL_SWING_CENTER);
                    break;
                case CLIMATE_SWING_VERTICAL:
					// When not auto we decide to set it to the center
                    SetHorizontalSwingControl(HORIZONTAL_SWING_CENTER);
					SetVerticalSwingControl(VERTICAL_SWING_AUTO);
                    break;
                case CLIMATE_SWING_HORIZONTAL:
                    SetHorizontalSwingControl(HORIZONTAL_SWING_AUTO);
					// When not auto we decide to set it to the center
					SetVerticalSwingControl(VERTICAL_SWING_CENTER);
                    break;
                case CLIMATE_SWING_BOTH:
                    SetHorizontalSwingControl(HORIZONTAL_SWING_AUTO);
					SetVerticalSwingControl(VERTICAL_SWING_AUTO);
                    break;
			}
			sendData(control_command, sizeof(control_command)); 
        }
		
		
		if (call.get_target_temperature().has_value()) {
		    float temp = *call.get_target_temperature();
			ESP_LOGD("Control", "*call.get_target_temperature() = %f", temp);
			control_command[SET_POINT_OFFSET] = (unsigned int) temp - 16;
			sendData(control_command, sizeof(control_command));			
			target_temperature = temp;
            this->publish_state();
		}
		
		
   }


    void sendData(byte * message, byte size) {
        byte crc_offset = CRC_OFFSET(message);
        byte crc = getChecksum(message, size);
        word crc_16 = crc16(0, &(message[2]), crc_offset-2);
        
        // Updates the crc
        message[crc_offset] = crc;
        message[crc_offset+1] = (crc_16>>8)&0xFF;
        message[crc_offset+2] = crc_16&0xFF;

        Serial.write(message, size);
        auto raw = getHex(message, size);
        ESP_LOGD("Haier", "Message sent: %s  - CRC: %X - CRC16: %X", raw.c_str(), crc, crc_16);

    }

    String getHex(byte * message, byte size) {


        String raw;

        for (int i=0; i < size; i++){
			raw += " " + String(message[i]);

        }
        raw.toUpperCase();

        return raw;


    }

    byte getChecksum(const byte * message, size_t size) {
		byte position = CRC_OFFSET(message);
        byte crc = 0;
        
        if (size < ( position)) {
        	ESP_LOGE("Control", "frame format error (size = %d vs length = %d)", size, message[2]);
        	return 0;
        }

        for (int i = 2; i < position; i++)
            crc += message[i];

        return crc;
    }


    unsigned crc16(unsigned crc, unsigned char *buf, size_t len)
    { 
        while (len--) {
            crc ^= *buf++;
            crc = crc & 1 ? (crc >> 1) ^ POLY : crc >> 1;
            crc = crc & 1 ? (crc >> 1) ^ POLY : crc >> 1;
            crc = crc & 1 ? (crc >> 1) ^ POLY : crc >> 1;
            crc = crc & 1 ? (crc >> 1) ^ POLY : crc >> 1;
            crc = crc & 1 ? (crc >> 1) ^ POLY : crc >> 1;
            crc = crc & 1 ? (crc >> 1) ^ POLY : crc >> 1;
            crc = crc & 1 ? (crc >> 1) ^ POLY : crc >> 1;
            crc = crc & 1 ? (crc >> 1) ^ POLY : crc >> 1;
        }
        return crc;
    }


};


#endif //HAIER_ESP_HAIER_H

Hi all,

To save anyone else having the trouble I’ve had most of today - figured I’d register an account and post.

I’m using 3 Haier AC units all running R_1.0.00/e_2.3.12 firmware, originally connected to SmartAir2.

I was having strange issues where my ESP8266 was sending all of the right commands and my ESPHome logs showed polling messages, but for some reason I’d never be able to use the integration as I would hit an error “No action, first poll answer not received” when trying to adjust the state of the A/C.

I spent some time debugging, and realised while sniffing with a UART that upon connecting the RX pin to my ESP8266, I was triggering the over-voltage snap-back circuit, which was grounding out the RX pin.

Basically, I could see response messages from my air-conditioner when only the UART was connected to the serial interface, but as soon as the ESP was connected too - dead silence.

Investigating Instalator’s circuit schematics I could see he was running RX through a series of voltage drop resistors. This wasn’t enough to resolve the issue for me, and I ended up testing with a SN74ahct125n level shifter - immediately started to work, but these are large quad channel units I had laying around and I needed two - one for RX and one for TX.

I’ve since ordered a sparkfun logic level converter with the BSS138 mosfets on board which are bi-directional. A single one of these boards should sort out my issues once and for all.

TL;DR: if you’re having issues with this integration not receiving response messages and you’ve tried a few different Haier.h file versions, you likely need a level shifter like I did.

Hi Everyone, Ive been working on this a little more and ended up rewriting a huge part of the current integration. I now have purify, self clean, steri clean, sleep, boost, quiet, display controls, and remote lock all working. Ive also worked to further the reverse engineering the communication messages.

Testing and feedback welcome. Timers and wifi light control are the main things i know are missing as of now.

4 Likes

Great work Ivo!

I did do some reverse engineering, although I couldn’t get it to work to send any changes back. But maybe you have something of my ‘bitmap’ here:

If not, no problem, checking for a minute can’t hurt.

I never used the integration you are working on, but might do in the future as i would like to do some automation with it, like drying when it’s > 80% relative humidity. Or using another temperature sensor not in the airco itself.

Interesting, I hadnt seen your project till now, I didnt realize it would be so easy to sniff it from the network side of things! Quick glance at things shows that the network packets in your project seem to start with the temperature at byte 92, and in the uart side of things in my project they seem to start at 12. But they look to be the same or very similar!

Try the self clean, lock, unit bits on yours, they should work. Look at the haier .h and try some bits to see if they work for you!

Are you sure that byte 104 is humidity (byte 24 for me)? I have that marked as unknown in mine, i was thinking it might be the outside temp or something.

Edit: Just tried misting some water around the ac while it was running to see if byte 24 will change, it did not. I see a value of about 70 decimal that goes to about 63 when running in heat mode (makes sense as the outside unit will get colder) with an outside temperature of about 0-5c.

It WAS easy until they killed it, or until the first firmware update was done because it lost the power.

Since then you first need some initialization or something before you can sniff anything. Connection will be killed when you don’t, or something.

Haven’t been able to make any working connection with it from my local LAN. It would be so nice if it would just work with the official adapter :grimacing:

Have you looked inside your dongle to see what chipset its using? If its esp based, reflashing it should be easy, no need to use the official firmware since we already know how to control all the functions, plus that keeps everything local instead of it pinging home to haiers home servers.

Hi all,

Further to my earlier post, I’ve determined the cause of the lack of RX.

On Wemos 3.0 boards, the serial driver has an extremely powerful pull up resistor that the air-conditioner cannot overcome.

After testing with BSS138 the issue remained, but looking into things further I couldnt understand why the SN74 worked and resolved the issue while the BSS138 logic shifting did not.

All in all - to anyone else having problems, do NOT buy D1 3.0 boards - buy the older style modules with the soldered-on ESP module. They don’t suffer from this issue on their serial line.

I ordered and tested these older modules - worked perfectly and immediately with the BSS138 handling logic shifting.

@ceriel I suggest you try older boards if you did not already resolve your issues.

Cheers

1 Like

Now struggling with the same setup of wemos D1 mini v3.0 and BSS138-based shifter and the same symptomatic (no TX from AC). Where’s the pull-up resistor you mentioned is located? Maybe we can replace it with the lower-valued resistor or add the pull-down on the hi voltage (AC) side to reduce the current needed to pull the line down?

@bearpawmaxim I personally didn’t look into it greatly, I researched around and found that it was part of the USB-Serial chip circuit on the board.

Namely I concluded it from this NodeMCU ESP8266 not respond AT - Everything ESP8266

There were other sites too with mentions of high pull.

I did try reply and find out transistor values as my electronics skills aren’t what they once were, but there’s a schematic for a potential solution on that forum post. Only unknown is what transistors to try.

The software part. Have implemented the solution where the software uart (by esphome) is used on D6(RX) and D7(TX) pins but still can’t get the transmission from AC… Sniffed the communication, and saw that the Wemos sends a ‘POLL’ command and AC responds to that command with bytes, but that bytes is not arriving to Wemos…

About hardware. I am using Sparkfun BSS138 level shifter on both RX and TX lines and can’t get the RX even when crossing the RX and TX lines with each other.
Also, have tried the divider on RX line like in the original Installator schematic https://blog.instalator.ru/wp-content/uploads/2016/07/db844688b6cc4090baa37009173340c8.jpg with no luck (and no RX :grinning: )…
The strange things is happening with this Wemos clone, so I decided to build the the iot-uni-dongle board https://github.com/dudanov/iot-uni-dongle

P.S.: a sniffer that was mentioned earlier in my post is built on ESP32 Nodemcu board with the same Sparkfun BSS138 level converter and is receiving data from BOTH RX AND TX lines successfully…

Hello, I’m using zigbee for most of the “smart home” things, and all the things I want to actually use. Unfortunately no luck so far with haiers ACs. Do you know if it is easy to modify the code from esp home to zigbee? Did anyone succeed with it in the past? Is this the thing DNIGEF ZigBee Conversion Serial Port TTL Uart Wireless PA Module CC2530+CC2591|Integrated Circuits| - AliExpress that I need?

Especially with currently rising electricity prices and as I use AC mainly as heat pump it would be nice to have some usable integrations, not what is in hOn or smartAir2 or in my case if some units use one app and others different, which makes it unusable really.¨

Zigbee Configurable Firmware v2.5 – Zigbee Hobbyist. Rock Pi 4 SBC is this relevant?

I believe it wouldn’t be suitable for controlling an ac, i fear certain features would not be natively supported by zigbee and youd have to workaround to make it work. I believe ZHA has support for zigbee thermostats, but youd have to figure out workarounds for everything else the ac can do.

If you wanted to do that, youd need to make a ZigBee device that translates ZigBee commands to the custom serial commands the AC expects. I have not seen many examples of custom zigbee devices.

If you have a wifi network, why not use esphome?

Hello! I understand that you are the last enthusiast who brought this firmware to mind, please tell me, I can use it in my esp8266 (esp-01s) board?

Yes, i currently use it on an esp01 board, i took the stock pcb, tapped into the 3.3v, gnd, rx, and tx from it, and tied its en line to disable the factory esp32 on it, then i had just enough room to squeeze the esp01 in there with the factory pcb.

Do you have a telegram? I would like to get some advice from you…