Skip to content

Instantly share code, notes, and snippets.

@daniel-dona
Created February 5, 2024 18:56
Show Gist options
  • Save daniel-dona/bea0773e3ec4147cd0a58150f055318d to your computer and use it in GitHub Desktop.
Save daniel-dona/bea0773e3ec4147cd0a58150f055318d to your computer and use it in GitHub Desktop.
Xiaomi Mi Air Purifier 3C
esphome:
name: purificador
name_add_mac_suffix: true
includes:
- mipurifier.h
esp32:
board: esp32dev
framework:
type: esp-idf
version: recommended
# Custom sdkconfig options
sdkconfig_options:
CONFIG_FREERTOS_UNICORE: y
CONFIG_COMPILER_OPTIMIZATION_SIZE: y
# Advanced tweaking options
advanced:
ignore_efuse_mac_crc: true
# Make sure logging is not using the serial port
logger:
# Enable Home Assistant API
api:
encryption:
key: "************"
ota:
password: "****"
wifi:
ssid: "*********"
password: "***********"
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Fallback Hotspot"
password: "Z9kMMI8QA1Em"
captive_portal:
uart:
id: uart_bus
tx_pin: GPIO17
rx_pin: GPIO16
baud_rate: 115200
# debug:
# direction: BOTH
# dummy_receiver: false
# after:
# delimiter: "\n"
# sequence:
# - lambda: UARTDebug::log_string(direction, bytes);
# Initialize our custom component
custom_component:
- lambda: |-
auto mipurifier = new MiPurifier(id(uart_bus));
App.register_component(mipurifier);
return {mipurifier};
components:
- id: mipurifier
# Main switch for turning on/off the unit
switch:
- platform: template
name: "Power"
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"
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: "Lock"
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"
id: mode_select
options:
- "auto"
- "sleep"
- "manual"
set_action:
- lambda: |-
auto c = static_cast<MiPurifier *>(mipurifier);
c->set_mode(x);
- platform: template
name: "Display Brightness"
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"
id: manualspeed
icon: mdi:speedometer
min_value: 1
max_value: 100
mode: slider
step: 1
set_action:
- lambda: |-
auto c = static_cast<MiPurifier *>(mipurifier);
c->set_manualspeed((int) ((x*19)+300));
# Expose measured environmental values, and remaining filter life
sensor:
- platform: custom
lambda: |-
auto c = static_cast<MiPurifier *>(mipurifier);
return {
c->airquality_sensor,
c->filterlife_sensor,
c->filterhours_sensor,
c->motorspeed_sensor
};
sensors:
- name: "Air quality (PM2.5)"
unit_of_measurement: "µg/m³"
device_class: pm25
state_class: "measurement"
- name: "Filter remaining"
unit_of_measurement: "%"
icon: mdi:air-filter
- name: "Filter used hours"
unit_of_measurement: "h"
icon: mdi:air-filter
- name: "Motor speed"
unit_of_measurement: "rpm"
icon: mdi:speedometer
#include "esphome.h"
class MiPurifier : public Component, public UARTDevice, public CustomAPIDevice {
public:
static const int max_line_length = 80;
char recv_buffer[max_line_length];
char send_buffer[max_line_length];
bool is_preset;
int last_41, last_21, last_24, last_34, last_61, last_81, last_72, last_93, last_43, last_91;
int update_interval = 5000;
Sensor *airquality_sensor = new Sensor();
Sensor *filterlife_sensor = new Sensor();
Sensor *filterhours_sensor = new Sensor();
Sensor *motorspeed_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\r");
}
void turn_off() {
strcpy(send_buffer, "down set_properties 2 1 false\r");
}
void enable_beeper() {
strcpy(send_buffer, "down set_properties 6 1 true\r");
}
void disable_beeper() {
strcpy(send_buffer, "down set_properties 6 1 false\r");
}
void lock() {
strcpy(send_buffer, "down set_properties 8 1 true\r");
}
void unlock() {
strcpy(send_buffer, "down set_properties 8 1 false\r");
}
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\r");
} else if (mode == "sleep") {
strcpy(send_buffer, "down set_properties 2 4 1\r");
} else if (mode == "manual") {
strcpy(send_buffer, "down set_properties 2 4 2\r");
}
}
void set_brightness(std::string brightness) {
if (brightness == "off") {
strcpy(send_buffer, "down set_properties 7 2 0\r");
} else if (brightness == "low") {
strcpy(send_buffer, "down set_properties 7 2 3\r");
} else if (brightness == "high") {
strcpy(send_buffer, "down set_properties 7 2 8\r");
}
}
void set_manualspeed(int speed) {
snprintf(send_buffer, max_line_length, "down set_properties 9 3 %i\r", speed);
}
void send_command(std::string s) {
s.append("\r");
ESP_LOGD("purifier", s.c_str());
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, "41") == 0) {
filterlife_sensor->publish_state(atof(val));
} else if (strcmp(id, "43") == 0) {
filterhours_sensor->publish_state(atoi(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("sleep");
break;
case 2:
mode_select->publish_state("manual");
break;
case 3:
is_preset = true;
break;
}
} else if (strcmp(id, "61") == 0) {
// beeper (on, off)
beeper_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, "72") == 0) {
// display brightness (off, low, high)
switch (atoi(val)) {
case 8:
brightness_select->publish_state("high");
break;
case 3:
brightness_select->publish_state("low");
break;
case 0:
brightness_select->publish_state("off");
break;
}
} else if (strcmp(id, "93") == 0) {
// manual speed
manualspeed->publish_state((int)((atoi(val) - 300)/19));
} else if (strcmp(id, "91") == 0) {
motorspeed_sensor->publish_state(atoi(val));
// current speed
}
}
void setup() override {
register_service(&MiPurifier::send_command, "send_command", {"command"});
// get initial state & settings
strcpy(send_buffer, "down get_properties 2 1 2 4 3 4 4 1 6 1 7 2 8 1 9 3");
//strcpy(send_buffer, "down set_properties 9 4 1\r");
//strcpy(send_buffer, "downdown MIIO_net_change local");
}
/*else if (millis() - last_heartbeat > 60000) {
// send mysterious heartbeat message
write_str("down set_properties 9 4 60\r");
last_heartbeat = millis();
ESP_LOGD("purifier", "sent heartbeat");
}*/
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\r");
} else if (strcmp(cmd, "time") == 0) {
write_str("0\r");
} else if (strcmp(cmd, "mac") == 0) {
write_str("5448e6c6f5e3\r");
}else if (strcmp(cmd, "get_down") == 0) {
//ESP_LOGD("mipurifier", "get_down");
// send command from send_buffer
if (strlen(send_buffer) > 0) {
write_str(send_buffer);
send_buffer[0] = '\0';
ESP_LOGD("mipurifier", "sent send_buffer");
} else if (millis() - last_41 > update_interval) {
// force sensor update
write_str("down get_properties 4 1\r");
last_41 = millis();
} else if (millis() - last_21 > update_interval) {
// force sensor update
write_str("down get_properties 2 1\r");
last_21 = millis();
} else if (millis() - last_24 > update_interval) {
// force sensor update
write_str("down get_properties 2 4\r");
last_24 = millis();
} else if (millis() - last_34 > update_interval) {
// force sensor update
write_str("down get_properties 3 4\r");
last_34 = millis();
} else if (millis() - last_61 > update_interval) {
// force sensor update
write_str("down get_properties 6 1\r");
last_61 = millis();
} else if (millis() - last_81 > update_interval) {
// force sensor update
write_str("down get_properties 8 1\r");
last_81 = millis();
} else if (millis() - last_72 > update_interval) {
// force sensor update
write_str("down get_properties 7 2\r");
last_72 = millis();
} else if (millis() - last_93 > update_interval) {
// force sensor update
write_str("down get_properties 9 3\r");
last_93 = millis();
} else if (millis() - last_43 > update_interval) {
// force sensor update
write_str("down get_properties 4 3\r");
last_43 = millis();
} else if (millis() - last_91 > update_interval) {
// force sensor update
write_str("down get_properties 9 1\r");
last_91 = millis();
}else {
write_str("down none\r");
}
} 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);
ESP_LOGD("mipurifier", "property change %s %s", id, val);
write_str("ok\r");
} 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);
ESP_LOGD("mipurifier", "result %s %s", id, val);
}
write_str("ok\r");
} else {
// just acknowledge any other message
write_str("ok\r");
}
}
}
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment