ESPHome Builer - MHI update failing due to configuration

Hi all,

Some time ago a component of my Mitsubishi ac units was end of life and had to be replaced. This was fine after some changes but now I have an error again regarding the .yaml.
When I try to update I get the following error:

INFO ESPHome 2025.11.1
INFO Reading configuration /config/esphome/badkamer-airco.yaml...
ERROR Unable to load component custom.climate:
Traceback (most recent call last):
  File "/esphome/esphome/loader.py", line 199, in _lookup_module
    module = importlib.import_module(f"esphome.components.{domain}")
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 999, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/data/external_components/da3a5f72/components/custom/climate/__init__.py", line 14, in <module>
    cv.Required(CONF_CLIMATES): cv.ensure_list(climate.CLIMATE_SCHEMA),
                                               ^^^^^^^^^^^^^^^^^^^^^^
AttributeError: module 'esphome.components.climate' has no attribute 'CLIMATE_SCHEMA'. Did you mean: '_CLIMATE_SCHEMA'?
Failed config

Platform not found: 'climate.custom'

This is the .yaml:

substitutions:
  # Unique device ID in HA
  deviceid: "badkamer_airco" #deviceid: "mhi_woonkamer"
  # Unique device name in HA (sensor names will be prefixed by this name)
  devicename: "Badkamer airco" #devicename: "MHI-Woonkamer"

esphome:
  name: badkamer-airco #name: mhi-woonkamer  # Unique name  (only change in small letters)
  platformio_options:
    # Run CPU at 160Mhz to fix mhi_ac_ctrl_core.loop error: -2
    board_build.f_cpu: 160000000L
  includes:
    - mhi_ac_ctrl.h
    - MHI-AC-Ctrl-core.h
    - MHI-AC-Ctrl-core.cpp
esp8266:
  board: d1_mini
  framework:
    version: recommended

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  #ssid: "hierjesidd"
  #password: "hierjeww"
  fast_connect: true  # misschien hier een hekje voor zetten?
  manual_ip:
    # Set this to the IP of the ESP
    static_ip: 192.168.1.88
    # Set this to the IP address of the router. Often ends with .1
    gateway: 192.168.1.1
    # The subnet of the network. 255.255.255.0 works for most home networks.
    subnet: 255.255.255.0
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "${devicename}"
    password: "configesp"

captive_portal:

logger:
  level: DEBUG
  baud_rate: 0

globals:
  - id: room_temp_api_timeout
    type: int
    restore_value: no
    initial_value: '120'

ota:
- platform: esphome

web_server:
  port: 80

external_components:
  - source:
      type: git
      url: https://github.com/robertklep/esphome-custom-component
    components: [ custom, custom_component ]

climate:
  - platform: custom
    lambda: |-
      auto mhi_ac_ctrl = new MhiAcCtrl();
      App.register_component(mhi_ac_ctrl);
      return {mhi_ac_ctrl};

    climates:
      - name: "${devicename}"
        id: ${deviceid}

api:
  reboot_timeout: 0s
  services:
    # Call the set_api_room_temperature service from HA to override the room temperature
    # If a new value has not been received after room_temp_api_timeout seconds, it will fall back to internal sensor
    - service: set_api_room_temperature
      variables:
        value: float
      then:
        - lambda: |-
            return ((MhiAcCtrl*)id(${deviceid}))->set_room_temperature(value);
    # Call the set_vanes service from HA to set the vane position
    # Needed because the ESPHome Climate class does not support this natively
    # Possible values: 1-4: static positions, 5: swing, 0: unknown
    - service: set_vanes
      variables:
        value: int
      then:
        - lambda: |-
            return ((MhiAcCtrl*)id(${deviceid}))->set_vanes(value);

sensor:
  - platform: wifi_signal # Reports the WiFi signal strength/RSSI in dB
    name: ${devicename} WiFi Signal db
    id: wifi_signal_db
    update_interval: 60s
    entity_category: diagnostic

  - platform: copy # Reports the WiFi signal strength in %
    source_id: wifi_signal_db
    name: ${devicename} WiFi Signal
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: "%"
    entity_category: diagnostic

  - platform: uptime
    name: ${devicename} Uptime
    unit_of_measurement: days
    update_interval: 3600s
    accuracy_decimals: 1
    filters:
      - multiply: 0.000011574

  - platform: custom
    lambda: |-
      return ((MhiAcCtrl*)id(${deviceid}))->get_sensors();

    # Sensor names in HA, you can change these if you want
    # Don't delete them or change their position in the list
    sensors:
      - name: ${devicename} error code
      - name: ${devicename} outdoor temperature
      - name: ${devicename} return air temperature
      - name: ${devicename} outdoor unit fan speed
      - name: ${devicename} indoor unit fan speed
      - name: ${devicename} current power
      - name: ${devicename} compressor frequency
      - name: ${devicename} indoor unit total run time
      - name: ${devicename} compressor total run time
      - name: ${devicename} vanes
        id: vanes_UD_received
        on_value:
          then:
            - lambda: |-
                float received_value = id(vanes_UD_received).state;
                if (received_value == 1.0) {
                  id(fan_control_ud).publish_state("Up");
                } else if (received_value == 2.0) {
                  id(fan_control_ud).publish_state("Up/Center");
                } else if (received_value == 3.0) {
                  id(fan_control_ud).publish_state("Center/Down");
                } else if (received_value == 4.0) {
                  id(fan_control_ud).publish_state("Down");
                } else if (received_value == 5.0) {
                  id(fan_control_ud).publish_state("Swing");
                }

      - name: ${devicename} energy used
      - name: ${devicename} Indoor (U-bend) HE temp 1
      - name: ${devicename} Indoor (capillary) HE temp 2
      - name: ${devicename} Indoor (suction header) HE temp 3
      - name: ${devicename} Outdoor HE temp
      - name: ${devicename} Outdoor unit exp. valve
      - name: ${devicename} Outdoor unit discharge pipe
      - name: ${devicename} Outdoor unit discharge pipe super heat
      - name: ${devicename} Protection error state
      - name: ${devicename} Vanes Left Right
        id: vanes_LR_received
        on_value:
          then:
            - lambda: |-
                float received_value = id(vanes_LR_received).state;
                if (received_value == 1.0) {
                  id(fan_control_lr).publish_state("Left");
                } else if (received_value == 2.0) {
                  id(fan_control_lr).publish_state("Left/Center");
                } else if (received_value == 3.0) {
                  id(fan_control_lr).publish_state("Center");
                } else if (received_value == 4.0) {
                  id(fan_control_lr).publish_state("Center/Right");
                } else if (received_value == 5.0) {
                  id(fan_control_lr).publish_state("Right");
                } else if (received_value == 6.0) {
                  id(fan_control_lr).publish_state("Wide");
                } else if (received_value == 7.0) {
                  id(fan_control_lr).publish_state("Spot");
                } else if (received_value == 8.0) {
                  id(fan_control_lr).publish_state("Swing");
                }
      - name: ${devicename} 3D Auto
        id: Dauto_received
        on_value:
          then:
            - lambda: |-
                bool received_value = id(Dauto_received).state;
                if (received_value) {
                  ESP_LOGD("main", "received 3DAuto from AC");
                  id(fan_control_3Dauto).publish_state(true);
                }
                else {
                  ESP_LOGD("main", "received 3DAuto off from AC");
                  id(fan_control_3Dauto).publish_state(false);
                }

binary_sensor:
  - platform: custom
    lambda: |-
      return ((MhiAcCtrl*)id(${deviceid}))->get_binary_sensors();

    binary_sensors:
      - name: ${devicename} defrost

text_sensor:
  - platform: version
    name: ${devicename} ESPHome Version
  - platform: wifi_info
    ip_address:
      name: ${devicename} IP
    ssid:
      name: ${devicename} SSID
    bssid:
      name: ${devicename} BSSID
  - platform: custom
    lambda: |-
      return ((MhiAcCtrl*)id(${deviceid}))->get_text_sensors();

    text_sensors:
      - name: ${devicename} Protection error state

select:
  - platform: template
    name: ${devicename} Fan Control Left Right
    id: fan_control_lr
    icon: mdi:arrow-left-right
    optimistic: true
    options:
      # - "3D Auto"
      - "Left"
      - "Left/Center"
      - "Center"
      - "Center/Right"
      - "Right"
      - "Wide"
      - "Spot"
      - "Swing"
    on_value:
      - lambda: |-
          auto state = id(fan_control_lr).state.c_str();
          ESP_LOGD("main", "Option of my select: %s", state);
          uint8_t vanesLR = 0;  // Initialize the vanesLR variable
          if (id(fan_control_lr).state == "3D Auto") {
            id(fan_control_3Dauto).publish_state(true);
          } else if (id(fan_control_lr).state == "Left") {
            vanesLR = 1;
          } else if (id(fan_control_lr).state == "Left/Center") {
            vanesLR = 2;
          } else if (id(fan_control_lr).state == "Center") {
            vanesLR = 3;
          } else if (id(fan_control_lr).state == "Center/Right") {
            vanesLR = 4;
          } else if (id(fan_control_lr).state == "Right") {
            vanesLR = 5;
          } else if (id(fan_control_lr).state == "Wide") {
            vanesLR = 6;
          } else if (id(fan_control_lr).state == "Spot") {
            vanesLR = 7;
          } else if (id(fan_control_lr).state == "Swing") {
            vanesLR = 8;
          }
          if ((vanesLR > 0) & (vanesLR < 9) & (vanesLR != id(vanes_LR_received).state)){
            ESP_LOGD("main", "setting vanesLR to: %i", vanesLR);
            return ((MhiAcCtrl*)id(${deviceid}))->set_vanesLR(vanesLR);
          }
          else {
            ESP_LOGD("main", "Not setting vanesLR: %i", vanesLR);
          }

  - platform: template
    name: ${devicename} Fan Control Up Down
    id: fan_control_ud
    icon: mdi:arrow-up-down
    optimistic: true
    options:
      # - "3D Auto"
      - "Up"
      - "Up/Center"
      - "Center/Down"
      - "Down"
      - "Swing"
    on_value:
      - lambda: |-
          auto state = id(fan_control_lr).state.c_str();
          ESP_LOGD("main", "Option of my select: %s", state);
          uint8_t vanesUD = 0;  // Initialize the vanesUD variable
          if (id(fan_control_ud).state == "3D Auto") {
            id(fan_control_3Dauto).publish_state(true);
          } else if (id(fan_control_ud).state == "Up") {
            vanesUD = 1;
          } else if (id(fan_control_ud).state == "Up/Center") {
            vanesUD = 2;
          } else if (id(fan_control_ud).state == "Center/Down") {
            vanesUD = 3;
          } else if (id(fan_control_ud).state == "Down") {
            vanesUD = 4;
          } else if (id(fan_control_ud).state == "Swing") {
            vanesUD = 5;
          }
          if ((vanesUD > 0) & (vanesUD < 6) & (vanesUD != id(vanes_UD_received).state)){
            ESP_LOGD("main", "setting vanesUD to: %i", vanesUD);
            return ((MhiAcCtrl*)id(${deviceid}))->set_vanes(vanesUD);
          }
          else {
            ESP_LOGD("main", "Not setting vanesUD: %i", vanesUD);
          }

switch:
  - platform: template
    name: ${devicename} 3D Auto
    id: fan_control_3Dauto
    icon: mdi:video-3d
    optimistic: true
    turn_on_action:
      - lambda: |-
          if (id(Dauto_received).state !=1){
            ESP_LOGD("main", "Turn on 3DAuto");
            return ((MhiAcCtrl*)id(${deviceid}))->set_3Dauto(1);
          }
    turn_off_action:
      - lambda: |-
          if (id(Dauto_received).state !=0){
            ESP_LOGD("main", "Turn off 3DAuto");
            return ((MhiAcCtrl*)id(${deviceid}))->set_3Dauto(0);
          }

Can anyone point me in the right direction? I don’t like messing with the configuration too much unless I know exactly what I’m doing… Thanks a lot guys!

The custom component you use make use of a deprecated feature.
It is already reported to the maintainer, but if it gets solved is a bit uncertain.
There is maybe a workaround in the issue report thread.

In general updating ESPHome just to stay up-to-date is not advised.
If there are no new functionality or no bugs fixed for your use case then don’t update.
1 update per ½ year should be fine to not drop too far behind.

Thanks for the response! When it was first announced, I thought this was the work around:

external_components:
  - source:
      type: git
      url: https://github.com/robertklep/esphome-custom-component
    components: [ custom, custom_component ]

Thats also mentioned here: GitHub - igorlistopad/esphome-custom-components: Brings back support for custom and custom_component to ESPHome

But this is also deprecated?

That was the workaround in 2024.2, but there is a deprecation in the recent version also.

1 Like

Ah alright, so for now we have to wait for a fix for the workaround? :stuck_out_tongue: Or any other way to resolve this?

Look in the link I posted.

I totally agree that leapfrogging versions isn’t a bad practice. Depending on the situation, I may “advise” it myself.

But this issue is a good example of why updating regularly is a good thing. It lets you catch deprecations like this before they go unsupported. You can give yourself time to deal with them. For me it’s much easier to deal with a few minor issues each month than to save them all up and have to face them all at once.

So, my advice is to neither to jump on every version, nor wait six months or more before updating. Watch this forum for experience with each month’s version, and decide when it’s convenient for you to update. Personally I tend to update monthly after a couple of “point” releases. Not because I have to, but just to put any potential issues from that version behind me, so I won’t have to deal with them sometime in the future.

Obviously mine is not the only way, or probably even the officially “advised” way. But neither is waiting six or more months.

Sometimes the workaround and fixes for the deprecations takes time to develop too, so waiting often provide with a more stable and broader selection of choices.

1 Like