Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Air Purifier 4 Pro #14

Open
moto25 opened this issue Jan 17, 2024 · 10 comments
Open

Air Purifier 4 Pro #14

moto25 opened this issue Jan 17, 2024 · 10 comments

Comments

@moto25
Copy link

moto25 commented Jan 17, 2024

Hi, i just disassembled my new purifier, model AC-M15-SC. It hasnt STM32 buy GD32 controller. Will your code work on that controller too? thanx
PXL_20240117_035724511
PXL_20240117_035709303

@jaromeyer
Copy link
Owner

Hi, that's an interesting find. The GD32 basically is a chinese clone of the STM32 (there are some minor differences) so I would assume that they run almost the same firmware. Do you have a logic analyzer or another way (e.g. two UART-USB adapters) to analyze the communication between ESP and GD32?

@moto25
Copy link
Author

moto25 commented Jan 19, 2024

I will try it and let u know. BTW Mi 4 Lite from /issue/3 is working with patch thanx to @dhewg so u could add it to the list of supported devices.

@dhewg
Copy link

dhewg commented Jan 19, 2024

The 4lite patch for this repo can't be merged without breaking the 3h support, so the commited state doesn't support the 4lite.
That and the fact that custom components (as used here) are deprecated and support will apparently be removed from esphome at some point is the reason why I used the same approach as the upstream tuya support for esphome-miot.

As for the 4pro, it's probably enough to check the serial output to see if this is still using the same protocol. If it is, you can use esphome-miot and its miot spec to add support with only a yaml file.

@Matthijs33
Copy link

Hi,

I also flashed my Xiaomi Air Purifier 4 with board "ACM16-AA-B01" with Jaromeyer's project.

It took me a while to get things working, but I finally succeeded.
I also managed to add a filter reset push button to this project and some other functions that come with this model, such as Anion

P.S,
The GD32 chip is also on my board

mipurifier.h:


#include "esphome.h"

class MiPurifier : public Component, public UARTDevice, public CustomAPIDevice {
public:
  static const int max_line_length = 150;
  char recv_buffer[max_line_length];
  char send_buffer[max_line_length];
  bool is_preset;
  int last_heartbeat, last_query;
  bool initialValueSent = false;
  
  Sensor *airquality_sensor = new Sensor();
  Sensor *humidity_sensor = new Sensor();
  Sensor *temperature_sensor = new Sensor();
  Sensor *filterlife_sensor = new Sensor();
  Sensor *speed_sensor = new Sensor();

  MiPurifier(UARTComponent *uart) : UARTDevice(uart) {}

  int readline(int readch, char *buffer, int len) {
    static int pos = 0;
    int rpos;
    
    if (readch > 0) {
      switch (readch) {
        case '\r': // Return on CR
          rpos = pos;
          pos = 0;  // Reset position index ready for next time
          return rpos;
        default:
          if (pos < len-1) {
            buffer[pos++] = readch;
            buffer[pos] = 0;
          }
      }
    }
    // No end of line has been found, so return -1.
    return -1;
  }

  // only run setup() after a Wi-Fi connection has been established successfully
  float get_setup_priority() const override { return esphome::setup_priority::AFTER_WIFI; } 

  void turn_on() {
    strcpy(send_buffer, "down set_properties 2 1 true");
  }

  void turn_off() {
    strcpy(send_buffer, "down set_properties 2 1 false");
  }

  void enable_beeper() {
    strcpy(send_buffer, "down set_properties 6 1 true");
  }

  void disable_beeper() {
    strcpy(send_buffer, "down set_properties 6 1 false");
  }

  void enable_anion() {
    strcpy(send_buffer, "down set_properties 2 6 true");
  }

  void disable_anion() {
    strcpy(send_buffer, "down set_properties 2 6 false");
  }


  void lock() {
    strcpy(send_buffer, "down set_properties 8 1 true");
  }

  void unlock() {
    strcpy(send_buffer, "down set_properties 8 1 false");
  }

     // 10 1 1 - Set filter used hours back to 1 hour
  void reset_filter() {
    strcpy(send_buffer, "down set_properties 10 1 1");
  }

  void set_mode(std::string mode) {
    // 0: auto, 1: sleep, 2: manual, 3: low, 4: med, 5: high
    if (mode == "auto") {
      strcpy(send_buffer, "down set_properties 2 4 0");
    } else if (mode == "night") {
      strcpy(send_buffer, "down set_properties 2 4 1");
    } else if (mode == "manual") {
      strcpy(send_buffer, "down set_properties 2 4 2");
    } else if (mode == "low") {
      strcpy(send_buffer, "down set_properties 2 5 1");
    } else if (mode == "medium") {
      strcpy(send_buffer, "down set_properties 2 5 2");
    } else if (mode == "high") {
      strcpy(send_buffer, "down set_properties 2 5 3");
    }
  }

  void set_brightness(std::string brightness) {
    if (brightness == "off") {
      strcpy(send_buffer, "down set_properties 13 2 0");
    } else if (brightness == "low") {
      strcpy(send_buffer, "down set_properties 13 2 1");
    } else if (brightness == "high") {
      strcpy(send_buffer, "down set_properties 13 2 2");
    }
  }

// 2 4 2 = Set Manual Mode | 9 5 x = Set speed for Manual
  void set_manualspeed(int speed) {
    snprintf(send_buffer, max_line_length, "down set_properties 2 4 2, 9 5 %i", speed);
  }

  void send_command(std::string s) {
    strcpy(send_buffer, s.c_str());
  }

  void update_property(char* id, char* val) {
    if (strcmp(id, "34") == 0) {
      airquality_sensor->publish_state(atof(val));
    } else if (strcmp(id, "31") == 0) {
      humidity_sensor->publish_state(atof(val));
    } else if (strcmp(id, "37") == 0) {
      temperature_sensor->publish_state(atof(val));
    } else if (strcmp(id, "41") == 0) {
      filterlife_sensor->publish_state(atof(val));
    } else if (strcmp(id, "91") == 0) {
      speed_sensor->publish_state(atof(val));
    } else if (strcmp(id, "21") == 0) {
      // power (on, off)
      power_switch->publish_state(strcmp(val, "true") == 0);
    } else if (strcmp(id, "24") == 0) {
      // mode (auto, night, manual, preset)
      is_preset = false;
      switch (atoi(val)) {
        case 0:
          mode_select->publish_state("auto");
          break;
        case 1:
          mode_select->publish_state("night");
          break;
        case 2:
          mode_select->publish_state("manual");
          break;
        case 3:
          is_preset = true;
          break;
      }
    } else if (strcmp(id, "25") == 0) {
      // preset (low, medium, high)
      if (is_preset) {
        switch (atoi(val)) {
          case 1:
            mode_select->publish_state("low");
            break;
          case 2:
            mode_select->publish_state("medium");
            break;
          case 3:
            mode_select->publish_state("high");
            break;
        }
      }
    } else if (strcmp(id, "61") == 0) {
      // beeper (on, off)
      beeper_switch->publish_state(strcmp(val, "true") == 0);
    } else if (strcmp(id, "26") == 0) {
      // anion (on, off)
      anion_switch->publish_state(strcmp(val, "true") == 0);
    } else if (strcmp(id, "81") == 0) {
      // lock (on, off)
      lock_switch->publish_state(strcmp(val, "true") == 0);
    } else if (strcmp(id, "132") == 0) {
      // display brightness (off, low, high)
      switch (atoi(val)) {
        case 0:
          brightness_select->publish_state("off");
          break;
        case 1:
          brightness_select->publish_state("low");
          break;
        case 2:
          brightness_select->publish_state("high");
          break;
      }
    } else if (strcmp(id, "95") == 0) {
      // manual speed
      manualspeed->publish_state(atof(val)+1);
    }
  }

// get_down get_properties 2 1 = Power ON OFF State | 2 4 = Mode State | 6 1 = Beeper State | 13 2 = Display brigtness State | 8 1 = Lock State | 9 5 = Fan Speed State Range (1 - 11) | 9 1 = Fan RPM
  void setup() override {
    register_service(&MiPurifier::send_command, "send_command", {"command"});
  }
  
  void loop() override {
    while (available()) {
      if(readline(read(), recv_buffer, max_line_length) > 0) {
        char *cmd = strtok(recv_buffer, " ");
        if (strcmp(cmd, "net") == 0) {
            write_str("local");
        } else if (strcmp(cmd, "time") == 0) {
          write_str("0");
        } else if (strcmp(cmd, "get_down") == 0) {
          // send command from send_buffer
          if (strlen(send_buffer) > 0) {
            write_str(send_buffer);
			// Debug
            ESP_LOGD("mipurifier", send_buffer);
			// Debug
            send_buffer[0] = '\0';
            ESP_LOGD("mipurifier", "sent send_buffer");
          } else if (millis() - last_heartbeat > 60000) {
            // send heartbeat message
            write_str("down set_properties 11 6 300");
            last_heartbeat = millis();
            ESP_LOGD("purifier", "sent heartbeat");
          } else if (millis() - last_query > 60000) {
            // force sensor update
            write_str("down get_properties 3 4 3 1 3 7 4 1 9 1");
            last_query = millis();
            ESP_LOGD("purifier", "sent query string");
          } else {
            write_str("down none");
          }
        } else if (strcmp(cmd, "properties_changed") == 0) {
          ESP_LOGD("mipurifier", "parsing properties_changed message");
          char *id1 = strtok(NULL, " ");
          char *id2 = strtok(NULL, " ");
          char *id = strcat(id1, id2);
          char *val = strtok(NULL, " ");
          update_property(id, val);
          write_str("ok");
        } else if (strcmp(cmd, "result") == 0) {         
          // loop over all properties and update
          ESP_LOGD("mipurifier", "parsing result message");
          char *id1, *id2, *id, *val;
          while (true) {
            if (!(id1 = strtok(NULL, " "))) break;
            if (!(id2 = strtok(NULL, " "))) break;
            id = strcat(id1, id2);
            strtok(NULL, " "); // skip 0
            if (!(val = strtok(NULL, " "))) break;
            update_property(id, val);
          }
          write_str("ok");
        } else {
          // just acknowledge any other message
          write_str("ok");
        }
      }
    }
	
	  if( initialValueSent == false) {
		  strcpy(send_buffer, "down get_properties 2 1 2 4 2 6 6 1 13 2 8 1 9 5 9 1 3 4 3 1 3 7 4 1");
		  ESP_LOGD("mipurifier", "receiving states");
          initialValueSent = true;
        }
	
	
  }
};

and the .yaml file


esphome:
  name: nl-sp-06
  comment: Xiaomi Mi Air Purifier 4 (zhimi.airp.mb5)
  includes:
    - mipurifier.h

# Required configuration for the weird single core ESP-WROOM-32D module
esp32:
  board: esp32doit-devkit-v1
  framework:
    type: esp-idf
    sdkconfig_options:
      CONFIG_FREERTOS_UNICORE: y
    advanced:
      ignore_efuse_mac_crc: true




# Enable logging
logger:
#  level: ERROR

# Enable Home Assistant API
api:
  password: ""


# Enable OTA updates
ota:
  password: ""

web_server:
  port: 80


wifi:
  ap: {} # This spawns an AP with the device name and mac address with no password.
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  # Adapt to your needs
  use_address: 10.0.0.200
  manual_ip:
    static_ip: 10.0.0.200
    gateway: 10.0.0.1
    subnet: 255.255.255.0
    dns1: 10.0.0.1




# Initialize the serial connection to the GD32F303 ARM Controller
uart:
  id: uart_bus
  tx_pin: GPIO17
  rx_pin: GPIO16
  baud_rate: 115200

# Initialize our custom component
custom_component:
  - lambda: |-
      auto mipurifier = new MiPurifier(id(uart_bus));
      App.register_component(mipurifier);
      return {mipurifier};
    components:
      - id: mipurifier



button:
  - platform: template
    name: "Reset Filter Xiaomi P4"
    id: reset_switch
    on_press:
      - lambda:
          auto c = static_cast<MiPurifier *>(mipurifier);
          c->reset_filter();


# Main switch for turning on/off the unit
switch:
  - platform: template
    name: "Power Xiaomi P4"
    id: power_switch
    icon: mdi:power
    turn_on_action:
      - lambda:
          auto c = static_cast<MiPurifier *>(mipurifier);
          c->turn_on();
    turn_off_action:
      - lambda:
          auto c = static_cast<MiPurifier *>(mipurifier);
          c->turn_off();
  - platform: template
    name: "Beeper Xiaomi P4"
    id: beeper_switch
    icon: mdi:volume-high
    entity_category: config
    turn_on_action:
      - lambda:
          auto c = static_cast<MiPurifier *>(mipurifier);
          c->enable_beeper();
    turn_off_action:
      - lambda:
          auto c = static_cast<MiPurifier *>(mipurifier);
          c->disable_beeper();
  - platform: template
    name: "Anion Xiaomi P4"
    id: anion_switch
    icon: mdi:air-purifier
    turn_on_action:
      - lambda:
          auto c = static_cast<MiPurifier *>(mipurifier);
          c->enable_anion();
    turn_off_action:
      - lambda:
          auto c = static_cast<MiPurifier *>(mipurifier);
          c->disable_anion();
  - platform: template
    name: "Lock Xiaomi P4"
    id: lock_switch
    icon: mdi:lock
    entity_category: config
    turn_on_action:
      - lambda:
          auto c = static_cast<MiPurifier *>(mipurifier);
          c->lock();
    turn_off_action:
      - lambda:
          auto c = static_cast<MiPurifier *>(mipurifier);
          c->unlock();

# Select components for mode & brightness
select:
  - platform: template
    name: "Mode Xiaomi P4"
    id: mode_select
    options:
      - "auto"
      - "night"
      - "manual"
      - "low"
      - "medium"
      - "high"
    set_action:
      - lambda: |-
          auto c = static_cast<MiPurifier *>(mipurifier);
          c->set_mode(x);
  - platform: template
    name: "Display Brightness Xiaomi P4"
    id: brightness_select
    icon: mdi:brightness-6
    entity_category: config
    options:
      - "off"
      - "low"
      - "high"
    set_action:
      - lambda: |-
          auto c = static_cast<MiPurifier *>(mipurifier);
          c->set_brightness(x);

# Number to control the speed in manual mode
number:
  - platform: template
    name: "Manual Speed Xiaomi P4"
    id: manualspeed
    icon: mdi:speedometer
    min_value: 1
    max_value: 12
    step: 1
    set_action:
      - lambda: |-
          auto c = static_cast<MiPurifier *>(mipurifier);
          c->set_manualspeed((int)x-1);

# Expose measured environmental values, and remaining filter life
sensor:
  - platform: custom
    lambda: |-
      auto c = static_cast<MiPurifier *>(mipurifier);
      return {
        c->airquality_sensor,
        c->humidity_sensor,
        c->temperature_sensor,
        c->filterlife_sensor,
        c->speed_sensor,
      };
    sensors:
      - name: "Air quality (PM2.5) Xiaomi P4"
        unit_of_measurement: "µg/m³"
        device_class: pm25
      - name: "Humidity Xiaomi P4"
        unit_of_measurement: "%"
        device_class: humidity
      - name: "Temperature Xiaomi P4"
        unit_of_measurement: "°C"
        device_class: temperature
        accuracy_decimals: 1
      - name: "Filter remaining Xiaomi P4"
        unit_of_measurement: "%"
        icon: mdi:air-filter
      - name: "RPM Sensor Fan"
        unit_of_measurement: "RPM"


text_sensor:
  - platform: wifi_info
    ip_address:
      name: "IP Address Xiaomi P4"
    ssid:
      name: "Connected SSID Xiaomi P4"
    mac_address:
      name: "Mac Address Xiaomi P4"


And final some screenshots

image

image

@suaveolent
Copy link

Hi,

I also flashed my Xiaomi Air Purifier 4 with board "ACM16-AA-B01" with Jaromeyer's project.

It took me a while to get things working, but I finally succeeded. I also managed to add a filter reset push button to this project and some other functions that come with this model, such as Anion

Thank you so much! It works great on my Purifier 4.
This should be merged to the repo, maybe in a separate folder?

@Matthijs33
Copy link

Hi,
I also flashed my Xiaomi Air Purifier 4 with board "ACM16-AA-B01" with Jaromeyer's project.
It took me a while to get things working, but I finally succeeded. I also managed to add a filter reset push button to this project and some other functions that come with this model, such as Anion

Thank you so much! It works great on my Purifier 4. This should be merged to the repo, maybe in a separate folder?

Happy to help !
All credits to Jaromeyer

@mechanysm
Copy link

@Matthijs33 how did you get the purifier card setup? I've been trying to get it working with the HACSfrontend custom:purifier-card but it requires a fan entity which isn't present to add to the yaml config.

@Matthijs33
Copy link

Matthijs33 commented Feb 28, 2024

@Matthijs33 how did you get the purifier card setup? I've been trying to get it working with the HACSfrontend custom:purifier-card but it requires a fan entity which isn't present to add to the yaml config.

Hi @terryb8s
I have created an template in fan:

  - platform: template
    fans:
      xiaomi_purifier_4:
        friendly_name: "Xiaomi Air Purifier 4"
        value_template: "{{ states('switch.power_xiaomi_p4') }}"
        percentage_template: >
          {% if states('select.mode_xiaomi_p4', 'manual') == "manual" %}
            {% if states('number.manual_speed_xiaomi_p4') == "1.0"  %}
              8.33
            {% elif states('number.manual_speed_xiaomi_p4') == "2.0"  %}
              16.66
            {% elif states('number.manual_speed_xiaomi_p4') == "3.0"  %}
              24.99
            {% elif states('number.manual_speed_xiaomi_p4') == "4.0"  %}
              33.32
            {% elif states('number.manual_speed_xiaomi_p4') == "5.0"  %}
              41.65
            {% elif states('number.manual_speed_xiaomi_p4') == "6.0"  %}
              49.98
            {% elif states('number.manual_speed_xiaomi_p4') == "7.0"  %}
              58.31
            {% elif states('number.manual_speed_xiaomi_p4') == "8.0"  %}
              66.64
            {% elif states('number.manual_speed_xiaomi_p4') == "9.0"  %}
              74.97
            {% elif states('number.manual_speed_xiaomi_p4') == "10.0"  %}
              83.30
            {% elif states('number.manual_speed_xiaomi_p4') == "11.0"  %}
              91.63
            {% elif states('number.manual_speed_xiaomi_p4') == "12.0"  %}
              100
            {% else %}
              0.0
            {% endif %}
          {% elif states('select.mode_xiaomi_p4') == "low" %}
            24.99
          {% elif states('select.mode_xiaomi_p4') == "medium" %}
            41.65
          {% elif states('select.mode_xiaomi_p4') == "high" %}
            66.64
          {% elif states('select.mode_xiaomi_p4') == "night" %}
            8.33
          {% else %}
            0.0
          {% endif %}
        preset_mode_template: "{{ states('select.mode_xiaomi_p4') }}"
        turn_on:
          service: switch.turn_on
          target:
            entity_id: switch.power_xiaomi_p4
        turn_off:
          service: switch.turn_off
          target:
            entity_id: switch.power_xiaomi_p4
        set_percentage:
          service: script.xiaomi_set_speed
          data_template:
            percentage: "{{ percentage }}"
        set_preset_mode:
          service: select.select_option
          data:
            option: "{{ preset_mode }}"
          target:
            entity_id: select.mode_xiaomi_p4
        speed_count: 12
        preset_modes:
          - 'auto'
          - 'night'
          - 'manual'
          - 'low'
          - 'medium'
          - 'high'

Also i have created an script for the set speed / and the purifier will go off when i put the slider back to 0:

alias: Xiaomi Set Speed
fields:
  percentage:
    description: Percentage parsed from Fans.yaml
    example: "0"
sequence:
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ percentage == 0 }}"
        sequence:
          - service: switch.turn_off
            data: {}
            target:
              entity_id: switch.power_xiaomi_p4
      - conditions:
          - condition: template
            value_template: "{{ percentage > 0 }}"
        sequence:
          - service: number.set_value
            target:
              entity_id: number.manual_speed_xiaomi_p4
            data:
              value: >-
                {% set setspeed = percentage | float  /8.33 %} {{ ((setspeed) |
                round(0, default=0)) }}
mode: single

@mechanysm
Copy link

Thanks this is amazing, while my home assistant is becoming more complex by the day i'm still learning so appreciate the examples!Also thanks for everyone getting the 4 pro going!

@vijexa
Copy link

vijexa commented Mar 29, 2024

Thanks everyone, I also was able to flash 4 pro without any issues using code modified by @Matthijs33. Small note: when I powered the board with usb to ttl adapter, there were no beeps (says it should beep in readme), but I still was able to backup the firmware and flash esphome successfully.

As I spent an embarrassingly long time trying to disassemble this thing while breaking a few plastic tabs, I've recorded this quick video that shows how to do it for anyone that stumbles upon this https://www.youtube.com/watch?v=PHaw3gTkfxE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants