Custom ESPHome component for Levoit air purifiers (Core and Vital series) enabling local control without cloud dependency.
See Supported Models and Feature Matrix and Changelog
⚠️ Requires disassembly and serial access (TX, RX, GND, EN, GPIO0) initially
Flash ESPHome directly onto the factory ESP32-Solo-1 module using serial connection.
Keep the original ESP32 functional while adding a custom ESP32 for ESPHome control. This approach allows switching between firmware versions and enables MCU firmware updates.
Hardware Setup:
-
Install a 2-position switch to select which ESP32 is active, only use during power down!
-
Wire the switch:
- Common (middle): Connect to GND
- Position A: Connect to EN pin of original ESP32
- Position B: Connect to EN pin of new ESP32
-
Connect new ESP32:
- Power (3.3V) and GND from purifier PCB
- TX/RX to MCU UART pins (parallel to original ESP32)
Benefits:
- ✅ Revert to factory firmware anytime
- ✅ Perform official MCU firmware updates when needed
- ✅ Test ESPHome changes without risk
⚠️ Note: New MCU firmware may require protocol updates in this component
Recommended modules:
- XIAO Seeed ESP32-S3 - Compact form factor, easy to integrate
- XIAO Seeed ESP32-C3 - Budget-friendly alternative
- Any ESP32 module with UART and sufficient GPIO pins
Each model requires different disassembly procedures. See model-specific guides in projects/free-levoit for:
- PCB pinout diagrams
- Disassembly instructions
- UART pin locations
- Photos and wiring diagrams
In your ESPHome YAML configuration:
Local Development:
external_components:
- source:
type: local
path: ../../../components # Relative to your YAML file
components: [levoit]Production (GitHub):
external_components:
- source:
type: git
url: https://github.com/tuct/esphome-projects
ref: main
components: [levoit]For Core 300/400s and Levoit 100s/200s:
esp32: board: esp32dev framework: type: esp-idf sdkconfig_options: CONFIG_FREERTOS_UNICORE: y
for custom esp, set accordingly
Match your hardware connections:
uart:
tx_pin: GPIO17 # ESP TX → MCU RX for original ESP32!
rx_pin: GPIO16 # ESP RX → MCU TX for original ESP32!
baud_rate: 115200Specify your model (must match device):
levoit:
id: air_purifier
model: CORE300S # Options: VITAL100S, VITAL200S, CORE200S, CORE300S, CORE400S# Compile only (check for errors)
esphome compile your-config.yaml
# Compile and upload via serial
esphome run your-config.yaml
# Upload via OTA (after initial flash)
esphome upload your-config.yaml --device your-device.localFor complete working examples, see the free-levoit project configurations.
No communication with MCU:
- Verify TX/RX are not swapped (common mistake)
- Check 3.3V power supply voltage under load
- Enable UART debugging:
uart: debug: { direction: BOTH } - Confirm baud rate is 115200
Boot loops or crashes:
- Check
model:matches your actual device - Verify GPIO pins don't conflict with ESP32 bootstrap pins
- Try disabling PSRAM if using ESP32-S3:
psram_mode: disabled
Device not responding in Home Assistant:
- Confirm ESPHome API is enabled
- Check WiFi credentials and network connectivity
- Review logs:
esphome logs your-config.yaml - Verify model detection: Look for "Model set to: CORE300S (ModelType=2)" in logs
levoit/
├── levoit.cpp/.h # Main component, UART handling, message routing
├── levoit_message.h/.cpp # Message building and frame construction utilities
├── decoder.cpp/.h # Frame parsing and message dispatch
├── types.h # Enum definitions for commands and entity types
├── core_status.cpp/.h # Core series status/timer payload decoders
├── vital_status.cpp/.h # Vital series status payload decoders
├── core_commands.cpp/.h # Core series command builders (300S/400S)
├── vital_commands.cpp/.h # Vital series command builders (100S/200S)
├── decoder_helpers.h # Shared utility functions
├── tlv.cpp/.h # TLV encoding for complex payloads
└── [platform]/ # ESPHome platform integrations
├── fan/ # Fan entity
├── switch/ # Switch entities (display, lock, etc.)
├── number/ # Number entities (timer, room size)
├── select/ # Select entities (auto mode)
├── sensor/ # Sensor entities (PM2.5, CADR, filter life)
├── binary_sensor/ # Binary sensor entities (filter low status)
├── button/ # Button entities (filter reset)
└── text_sensor/ # Text sensor entities (timer display)
- Interface: UART at 115200 baud
- Frame Format:
A5 [type:3] [seq:1] [len:1] [reserved:1] [chk:1] [cmd:1] [payload:N] - Byte Order: Little-endian for multi-byte values
- Message Counter: Global sequence number (
messageUpCounter) tracks outgoing messages - Model Detection: Automatic based on initial handshake (TODO)
- Checksum: Sum of all bytes excluding checksum byte itself
external_components:
- source:
type: local
path: ../../../components # or use git source
components: [levoit]
uart:
tx_pin: GPIO4
rx_pin: GPIO5
baud_rate: 115200
levoit:
id: air_purifier
model: CORE300S # or VITAL100S, VITAL200S, CORE200S, CORE400S
fan:
- platform: levoit
levoit: air_purifier
name: "Air Purifier"
switch:
- platform: levoit
levoit: air_purifier
name: "Display"
type: display
- platform: levoit
levoit: air_purifier
name: "Child Lock"
type: child_lock
number:
- platform: levoit
levoit: air_purifier
name: "Timer"
type: timer
- platform: levoit
levoit: air_purifier
name: "Filter Lifetime (months)"
type: filter_lifetime_months
sensor:
- platform: levoit
levoit: air_purifier
name: "Current CADR"
type: current_cadr
- platform: levoit
levoit: air_purifier
name: "Filter Life Left"
type: filter_life_left
binary_sensor:
- platform: levoit
levoit: air_purifier
name: "Filter Low"
type: filter_low
button:
- platform: levoit
levoit: air_purifier
name: "Reset Filter Stats"
type: reset_filter_stats
text_sensor:
- platform: levoit
levoit: air_purifier
name: "Timer Remaining"
type: timer_duration_remaining
- platform: levoit
levoit: air_purifier
name: "MCU Version"
type: mcu_version
select:
- platform: levoit
levoit: air_purifier
name: "Night Light"
type: nightlight # Core200S onlyFor complete configuration examples, see the free-levoit project.
The Filter Life Left sensor tracks filter usage as a percentage (0-100%) based on cumulative CADR consumption.
-
Baseline Capacity:
- Model CADR (e.g., 214 m³/h for Core300S) multiplied by filter lifetime (default 12 months)
- Formula:
Total Capacity = CADR × 24 hours × 30 days × Filter Lifetime (months) - Example:
214 × 24 × 30 × 12 = 1,844,160 m³for Core300S at 12 months
-
Real-Time Tracking:
- Every minute, the component accumulates CADR based on current fan speed
- Tracks
used_cadr_(total m³ processed) persisted to device preferences - Updates
total_runtime_(minutes fan has been on)
-
Speed-Dependent CADR:
- Level 1:
CADR × 1 ÷ max_speed(derates to ~63% in Sleep mode) - Level 2:
CADR × 2 ÷ max_speed - Level 3:
CADR × 3 ÷ max_speed - Level 4:
CADR × 4 ÷ max_speed(Core400S/Vital series only)
- Level 1:
-
Percentage Calculation:
Filter Life % = 100 - (used_cadr ÷ Total Capacity × 100)- Clamped to 0-100% range
- Published every 10 seconds as a float with one decimal place
-
Binary Sensor Threshold:
- Filter Low binary sensor activates when
Filter Life % < 5% - Useful for automations (e.g., order replacement reminders)
- Filter Low binary sensor activates when
- Use the Reset Filter Stats button to reset
used_cadrandtotal_runtimeto 0 - This resets the filter life percentage back to 100%
- Persists immediately to device storage
Adjust filter lifetime expectancy via the Filter Lifetime (months) number entity:
number:
- platform: levoit
levoit: air_purifier
name: "Filter Lifetime (months)"
type: filter_lifetime_months
min_value: 1
max_value: 24
step: 1Default: 12 months. Adjust based on your filter's actual rated lifespan or usage pattern.
- Message Building: Centralized in
levoit_message.h/.cppwith inline functions for efficiency - Command Builders: Separated into
core_commands.cppandvital_commands.cppfor model-specific logic - Global Counter:
messageUpCountertracks outgoing message sequence (inline variable inlevoit_message.h)
- Add enum to
CommandTypein types.h - Implement in
build_core_command()orbuild_vital_command()depending on model series - Pattern: Define
msg_typeandpayloadvectors, returnbuild_levoit_message(msg_type, payload, messageUpCounter) - The
build_levoit_message()function handles frame construction, counter insertion, and checksum calculation
Enable verbose logging in your YAML:
logger:
level: VERBOSE
uart:
debug:
direction: BOTH # Monitor raw UART traffic- Implement custom sleep mode settings for Vital series
- Model-specific room size validation based on CADR ratings
- Verify Core 300S/400S protocol differences (MCU version dependency)
- WiFi LED control and status indication after connection
- Add filter life time and current CADR sensors
- Add filter low binary sensor (< 5% threshold)
- Add reset filter stats button
- Enable filter reset from Home Assistant
- Enable filter reset from Device (Long press sleep)
- Add appropriate icons for entities
- Test compatibility with ESPHome 2025.12+ (preset mode API changes)
- Update to ESPHome 2025.12.5 (completed)
Special thanks to the original developers who reverse-engineered the Levoit protocols:
This component is provided as-is for educational and personal use. Levoit and related trademarks are property of their respective owners.