Skip to content

Instantly share code, notes, and snippets.

@kklouzal
Last active July 6, 2025 17:12
Show Gist options
  • Select an option

  • Save kklouzal/0e6ccc4f8c730bcb032c832147775b79 to your computer and use it in GitHub Desktop.

Select an option

Save kklouzal/0e6ccc4f8c730bcb032c832147775b79 to your computer and use it in GitHub Desktop.
Mr. Cool (Midea) Minisplit ESPHome YAML - Intelligent Fan Control
# ESPHome Configuration for MRCOOL Minisplit Climate Control System
#
# OVERVIEW:
# This configuration implements an intelligent climate control system for a MRCOOL minisplit
# unit using ESP8266 with UART communication via the Midea protocol. The system features
# adaptive fan speed control, struggle detection, and energy optimization algorithms.
#
# KEY FEATURES:
# - Smart Fan Control: Automatically adjusts fan speed based on temperature delta and system performance
# - 5-Level Struggle Detection: Monitors system effectiveness and boosts fan speed when struggling
# - Adaptive Fan Speed Management: Uses 0.5°C to >2.0°C delta thresholds for fan speed selection
# - Energy Optimization: 3-minute cooldown periods prevent rapid fan speed oscillations
# - Mode-Aware Operation: Different logic for cooling vs heating modes, auto mode bypass
# - Temperature Bounds: Enforces 72-80°F (22.2-26.7°C) operating range
# - Resource Efficiency: Optimized update intervals and reduced logging for ESP8266 performance
#
# FAN SPEED LOGIC:
# - ≤0.5°C delta: Silent mode
# - ≤1.0°C delta: Low speed
# - ≤1.5°C delta: Medium speed
# - ≤2.0°C delta: High speed
# - >2.0°C delta: Turbo speed
# - Struggle boost: +1 to +4 levels when system struggles to reach target temperature
#
# STRUGGLE DETECTION:
# - Uses 15-minute rolling average of temperature delta
# - 5 levels (0-4) with 10-minute intervals for increases, 5-minute for decreases
# - Only considers positive delta (room warmer than target in cooling, cooler in heating)
# - Gradually reduces struggle level only with sustained good performance
#
# HARDWARE:
# - Platform: ESP8266 (ESP12E)
# - Communication: UART (TX:12, RX:14, 9600 baud)
# - Protocol: Midea IR protocol for AC control
# - Interface: Web server, API, OTA updates
# Device identification and naming substitutions
# These values are used throughout the configuration for consistent naming
substitutions:
node_name: minisplit-01 # ESPHome device name
node_id: midea_ac_01 # Unique identifier for entities
friendly_node_name: "Livingroom AC 1" # Human-readable name for UI
device_description: "MRCOOL Minisplit Climate Control"
# Global variables to maintain state across reboots and function calls
# These track the smart fan control system's current state
globals:
# Global variables to maintain state across reboots and function calls
# These track the smart fan control system's current state
# Current fan mode level (0-5): silent, Low, Medium, High, turbo, Auto
- id: ${node_id}_last_fan_mode
type: int
initial_value: '1' # 0=silent, 1=Low, 2=Medium, 3=High, 4=turbo, 5=Auto
# Struggle detection level (0-4): how hard the system is working to reach target
- id: ${node_id}_struggle_level
type: int
initial_value: '0' # 0-4 struggle levels
# Unified timer for both struggle increase and decrease operations
- id: ${node_id}_last_struggle_time
type: uint32_t
initial_value: '0' # Combined timer for both increase/decrease
# Cooldown timer to prevent rapid fan speed changes
- id: ${node_id}_last_fan_change_time
type: uint32_t
initial_value: '0'
# Tracks whether struggle level is currently increasing or decreasing
- id: ${node_id}_struggle_direction
type: bool
initial_value: 'false' # false=decreasing, true=increasing
# Core ESPHome configuration
esphome:
name: ${node_name}
comment: ${device_description}
project:
name: "mrcool.minisplit"
version: "1.0.0"
# ESP8266 hardware configuration
esp8266:
board: esp12e # NodeMCU/Wemos D1 Mini compatible
restore_from_flash: true # Restore states after power loss
# WiFi network configuration with fallback access point
wifi:
ssid: "SSID"
password: "PASS"
fast_connect: true # Skip network scan for faster connection
power_save_mode: none # Disable power saving for reliable communication
ap: # Fallback access point if WiFi fails
ssid: "${node_name} Fallback"
password: "slwf01pro"
# Logging configuration optimized for ESP8266 resources
logger:
baud_rate: 0 # Disable UART logging (using UART for AC communication)
level: WARN # Reduced from INFO to minimize resource usage
logs:
climate: WARN # Reduce climate component verbosity
midea: WARN # Reduce Midea protocol verbosity
# Web interface for device management
web_server:
port: 80
# Over-the-air update capability
ota:
platform: esphome
# Home Assistant API integration
api:
# UART communication with the minisplit unit
uart:
tx_pin: 12 # GPIO12 - Data to AC unit
rx_pin: 14 # GPIO14 - Data from AC unit
baud_rate: 9600 # Standard Midea protocol baud rate
stop_bits: 1
data_bits: 8
parity: NONE
# Main climate control entity using Midea protocol
climate:
- platform: midea
id: ${node_id}_ac
name: ${friendly_node_name}
period: 10s # Increased from 5s to reduce communication overhead
timeout: 2s # Reduced from 3s for faster response
num_attempts: 2 # Reduced from 3 to save resources
beeper: false # Disable beep sounds by default
autoconf: false # Manual configuration for better control
# Supported operating modes
supported_modes:
- COOL # Cooling mode
- HEAT # Heating mode
- HEAT_COOL # Auto mode (heat/cool as needed)
- FAN_ONLY # Fan only mode
# Custom fan speed modes beyond standard Low/Medium/High
custom_fan_modes:
- silent # Ultra-quiet operation
- turbo # Maximum cooling/heating power
# Dashboard display settings (Fahrenheit for user interface)
visual:
min_temperature: 72 °F # Uses fahrenheit for dashboard display only
max_temperature: 80 °F # Internal logic operates in Celsius
temperature_step: 1 °F # Temperature adjustment increment
# Vertical swing control for air distribution
supported_swing_modes:
- VERTICAL
# Outdoor temperature sensor (if available from AC unit)
outdoor_temperature:
name: "${friendly_node_name} Outdoor Temperature"
id: ${node_id}_outdoor_temp
filters:
- filter_out: nan # Remove invalid readings
- sliding_window_moving_average:
window_size: 3 # Smooth out temperature fluctuations
send_every: 3
# Binary sensors for system status monitoring
binary_sensor:
# Device connectivity status
- platform: status
name: "${friendly_node_name} Status"
id: ${node_id}_status
# Sensor entities for monitoring and smart control logic
sensor:
# Temperature delta calculation for smart fan control
# This sensor calculates the rolling average temperature difference
# used by the struggle detection and fan speed algorithms
- platform: template
name: "${friendly_node_name} Rolling Avg Delta"
id: ${node_id}_avg_delta
unit_of_measurement: "°C"
accuracy_decimals: 2
update_interval: 30s # Reduced frequency for resource efficiency
lambda: |-
auto climate = id(${node_id}_ac);
float cur_temp = climate->current_temperature;
float target = climate->target_temperature;
if (isnan(cur_temp) || isnan(target)) {
return {};
}
// Calculate mode-aware difference for struggle detection
// Only consider positive delta when system should be working harder
float diff = 0.0;
if (climate->mode == climate::CLIMATE_MODE_COOL) {
diff = (cur_temp > target) ? (cur_temp - target) : 0.0; // Room warmer than target
} else if (climate->mode == climate::CLIMATE_MODE_HEAT) {
diff = (cur_temp < target) ? (target - cur_temp) : 0.0; // Room cooler than target
}
return diff;
filters:
# 15-minute rolling average at 30-second intervals
- sliding_window_moving_average:
window_size: 30 # 30 samples × 30s = 15 minutes
send_every: 1
# Text sensors for displaying system status information
text_sensor:
# Display current AC operating mode in human-readable format
- platform: template
id: ${node_id}_ac_mode
name: "${friendly_node_name} Mode"
icon: "mdi:air-conditioner"
update_interval: 30s # Reduced frequency for efficiency
lambda: |-
// Convert numeric mode to readable text
static constexpr const char* MODE_NAMES[] = {"Off", "Unknown", "Cool", "Heat", "Auto", "Fan Only"};
int mode_idx = static_cast<int>(id(${node_id}_ac)->mode);
return {(mode_idx >= 0 && mode_idx < 6) ? MODE_NAMES[mode_idx] : "Unknown"};
# Display current fan speed setting
- platform: template
id: ${node_id}_ac_speed
name: "${friendly_node_name} Fan Speed"
icon: "mdi:fan"
update_interval: 30s # Reduced frequency for efficiency
lambda: |-
// Convert numeric fan mode to readable text
static constexpr const char* FAN_MODE_NAMES[] = {"silent", "Low", "Medium", "High", "turbo", "Auto"};
int mode = id(${node_id}_last_fan_mode);
return {(mode >= 0 && mode <= 5) ? FAN_MODE_NAMES[mode] : "Unknown"};
# Control switches for user interaction and system configuration
switch:
# Manual control of AC beeper/notification sounds
- platform: template
id: ${node_id}_ac_beeper
name: "${friendly_node_name} Beeper"
icon: "mdi:volume-high"
optimistic: true # Assume command succeeds for UI responsiveness
restore_mode: RESTORE_DEFAULT_OFF
turn_on_action:
- midea_ac.beeper_on: # Enable AC unit beeper
turn_off_action:
- midea_ac.beeper_off: # Disable AC unit beeper
# Enable/disable the intelligent fan speed control system
- platform: template
id: ${node_id}_smart_fan_control
name: "${friendly_node_name} Smart Fan Control"
icon: "mdi:fan-auto"
optimistic: true
restore_mode: RESTORE_DEFAULT_ON # Enable smart control by default
# Smart fan control automation - runs every 30 seconds
# This is the heart of the intelligent climate control system
interval:
- interval: 30s # Reduced from 15s to save CPU cycles
then:
# Only run smart control if the user has enabled it
- if:
condition:
switch.is_on: ${node_id}_smart_fan_control
then:
- lambda: |-
auto climate = id(${node_id}_ac);
uint32_t now = millis();
// EARLY EXIT CONDITIONS
// Handle auto mode (HEAT_COOL) - bypass all smart logic and use "Auto" fan mode
if (climate->mode == climate::CLIMATE_MODE_HEAT_COOL) {
if (id(${node_id}_last_fan_mode) != 5 &&
(now - id(${node_id}_last_fan_change_time) >= 180000)) { // 3-minute cooldown
auto call = climate->make_call();
call.set_fan_mode("Auto");
call.perform();
id(${node_id}_last_fan_mode) = 5;
id(${node_id}_last_fan_change_time) = now;
}
return;
}
// Skip processing for fan-only and off modes
if (climate->mode == climate::CLIMATE_MODE_FAN_ONLY ||
climate->mode == climate::CLIMATE_MODE_OFF) {
return;
}
// TEMPERATURE VALIDATION AND BOUNDS CHECKING
float cur_temp = climate->current_temperature;
float target = climate->target_temperature;
if (isnan(cur_temp) || isnan(target)) {
return; // Skip if temperature readings are invalid
}
// Enforce temperature bounds (72-80°F converted to Celsius)
constexpr float MIN_TEMP = 22.2f; // 72°F in Celsius
constexpr float MAX_TEMP = 26.7f; // 80°F in Celsius
if (target < MIN_TEMP) {
target = MIN_TEMP;
auto call = climate->make_call();
call.set_target_temperature(MIN_TEMP);
call.perform();
} else if (target > MAX_TEMP) {
target = MAX_TEMP;
auto call = climate->make_call();
call.set_target_temperature(MAX_TEMP);
call.perform();
}
// GET ROLLING AVERAGE DELTA FOR STRUGGLE DETECTION
float avg_delta = id(${node_id}_avg_delta).state;
if (isnan(avg_delta)) {
return;
}
// STRUGGLE DETECTION ALGORITHM
// Tracks how hard the system is working to reach target temperature
if (avg_delta > 0.5f) {
// Poor performance detected - consider increasing struggle level
if (!id(${node_id}_struggle_direction)) {
// Switch from decreasing to increasing mode
id(${node_id}_struggle_direction) = true;
id(${node_id}_last_struggle_time) = now;
} else if (now - id(${node_id}_last_struggle_time) >= 600000 && // 10 minutes
id(${node_id}_struggle_level) < 4) {
// Increase struggle level after sustained poor performance
id(${node_id}_struggle_level)++;
id(${node_id}_last_struggle_time) = now;
}
} else {
// Good performance detected - consider decreasing struggle level
if (id(${node_id}_struggle_direction)) {
// Switch from increasing to decreasing mode
id(${node_id}_struggle_direction) = false;
id(${node_id}_last_struggle_time) = now;
} else if (now - id(${node_id}_last_struggle_time) >= 300000 && // 5 minutes
id(${node_id}_struggle_level) > 0) {
// Decrease struggle level after sustained good performance
id(${node_id}_struggle_level)--;
id(${node_id}_last_struggle_time) = now;
}
}
// FAN SPEED CALCULATION ALGORITHM
// Base fan speed determination using temperature delta thresholds
static constexpr float DELTA_THRESHOLDS[] = {0.5f, 1.0f, 1.5f, 2.0f};
static constexpr const char* FAN_MODES[] = {"silent", "Low", "Medium", "High", "turbo"};
int base_level = 4; // Default to turbo for high delta
for (int i = 0; i < 4; i++) {
if (avg_delta < DELTA_THRESHOLDS[i]) {
base_level = i;
break;
}
}
// Apply struggle boost when system is working hard
// Only boost when delta indicates poor performance (≥0.5°C)
int final_level = (avg_delta >= 0.5f) ?
std::min(4, base_level + id(${node_id}_struggle_level)) : base_level;
// APPLY FAN SPEED CHANGE WITH COOLDOWN
// Only change fan speed if different from current and cooldown period has passed
if (id(${node_id}_last_fan_mode) != final_level &&
(now - id(${node_id}_last_fan_change_time) >= 180000)) { // 3-minute cooldown
auto call = climate->make_call();
call.set_fan_mode(FAN_MODES[final_level]);
call.perform();
id(${node_id}_last_fan_mode) = final_level;
id(${node_id}_last_fan_change_time) = now;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment