Created
February 5, 2024 18:56
-
-
Save daniel-dona/bea0773e3ec4147cd0a58150f055318d to your computer and use it in GitHub Desktop.
Xiaomi Mi Air Purifier 3C
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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