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