Last active
July 6, 2025 17:12
-
-
Save kklouzal/0e6ccc4f8c730bcb032c832147775b79 to your computer and use it in GitHub Desktop.
Mr. Cool (Midea) Minisplit ESPHome YAML - Intelligent Fan Control
This file contains hidden or 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 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