ESPHome Nest thermostat clone on cheap rotary display

For windows command line isn’t it as simple as the first (windows) section here :

https://esphome.io/guides/installing_esphome/

I use Linux as per that guide BTW and everything works as expected.

Thanks for the suggestion!

I’ve already successfully installed ESPHome following those instructions - Python and ESPHome are installed and working fine. I can run esphome version without any issues.

The problem occurs during compilation, not during installation. Specifically:

  1. :white_check_mark: ESPHome is installed correctly (version 2025.11.0-dev)
  2. :white_check_mark: Python 3.11 is installed and in PATH
  3. :white_check_mark: I can run esphome compile nesp.yaml - it starts compiling
  4. :x: The compilation fails when ESPHome tries to download ESP-IDF tools

The exact error:

INFO Installing tools via idf_tools.py (this may take several minutes)...
ERROR idf_tools.py installation failed (rc=1). Tail:
96%
97%
98%
99%
Done
INFO Installing tools via idf_tools.py (this may take several minutes)...

This happens on:

  • Windows 11 PC (following the official install guide)
  • Windows Surface laptop
  • Home Assistant ESPHome add-on on Raspberry Pi

All three environments fail at the exact same point - when idf_tools.py tries to install ESP-IDF framework tools. The download reaches 96-99% then fails and retries indefinitely.

I’ve also tried:

  • Multiple ESPHome versions
  • Docker with official ESPHome images
  • Manually installing ESP-IDF v5.2.6
  • Creating junctions from PlatformIO to installed ESP-IDF
  • Clearing all caches

The issue appears to be specific to ESP-IDF tools download, not ESPHome installation itself. Has anyone successfully compiled neo-nesp (or any ESP-IDF based ESPHome project) on Windows recently?

I got it working using windows. I needed to install python 3.11.9 as I couldn’t get it to work with the current release. Also git needs to be installed (as per the link from my previous post) and its location in your path.

Next you need to make sure there is no space in your platformIO path or you will get a corresponding error. see : Error: Detected a whitespace character in project paths - #7 by xumixu - PlatformIO IDE - PlatformIO Community you can set a (Windows) environment variable “PLATFORMIO_CORE_DIR ” to change the directory to be used.

First compile took a long time for tools installation probably due to virus/malware scanning in the background.

Thanks so much for taking the time to share your solution! I really appreciate it.

I’ve followed all your suggestions on a fresh Windows Surface Pro:

What I Did (Following Your Steps):

:white_check_mark: Python 3.11.9 - Installed from python.org :white_check_mark: Git - Installed and verified in PATH (git version 2.51.2.windows.1) :white_check_mark: PLATFORMIO_CORE_DIR - Set to C:\platformio (no spaces) :white_check_mark: Windows Defender - Disabled real-time protection and added exclusion for C:\platformio :white_check_mark: ESPHome - Installed via pip (esphome version 2025.10.5)

The Error I’m Getting:

Despite following all your steps, compilation consistently fails with:

INFO Installing tools via idf_tools.py (this may take several minutes)...
ERROR idf_tools.py installation failed

Then CMake errors:

CMake Error: The CMAKE_C_COMPILER (xtensa-esp32s3-elf-gcc) is not a full path 
and was not found in the PATH.

What I’ve Discovered:

The root issue appears to be that the pioarduino toolchain ZIP is corrupt - when downloaded, it only contains package.json and tools.json, with no actual compiler binaries.

I can see PlatformIO trying to download:

https://github.com/pioarduino/registry/releases/download/0.0.1/xtensa-esp-elf-14.2.0_20241119.zip

The download shows 100% complete, but when extracted, there are no binaries - just metadata files.

What I’ve Tried to Fix It:

  1. :white_check_mark: Manually downloaded the official Espressif toolchain from GitHub
  2. :white_check_mark: Extracted it to C:\platformio\packages\toolchain-xtensa-esp-elf
  3. :white_check_mark: Verified the compiler exists at .../bin/xtensa-esp32s3-elf-gcc.exe
  4. :x: BUT - PlatformIO keeps re-downloading and overwriting with the broken pioarduino version

Even when I lock the directory or add the toolchain to PATH, PlatformIO insists on downloading the corrupt pioarduio version during compilation.

Questions for You:

Since you got it working, could you help me understand:

  1. What ESPHome version did you use? (I’m on 2025.10.5)
  2. What does your toolchain directory contain? Can you check:
   dir C:\platformio\packages\toolchain-xtensa-esp-elf\bin

Does it have xtensa-esp32s3-elf-gcc.exe?

  1. Did you modify the platform: line in nesp.yaml? Mine currently points to:
   platform: https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21-2/platform-espressif32.zip
  1. Did you have any previous ESP-IDF or PlatformIO installations that might have cached working toolchains?
  2. Can you share the output from when your compilation succeeded? Specifically the “Installing tools via idf_tools.py” section?
  3. Did you do anything special to prevent PlatformIO from overwriting the toolchain?

What Worked on Your System?

I’m particularly interested in:

  • Whether the pioarduino toolchain downloaded successfully for you
  • If you had to manually fix anything with the toolchain
  • What your full compilation output looked like

I’ve tried this on three different devices now (Windows PC, Surface, Home Assistant) and all hit the same corrupt toolchain issue. Since you got it working, there must be something different about your setup or approach.

Any help would be greatly appreciated! This has been going on for days now and I’m at my wit’s end.

Thanks again for your help!

I can answer all the questions in your last post but I suspect the problem may be your config(s). I see you are trying to compile nesp.yaml but I don’t see that file in any of the 3 githubs linked in this thread. Can you please post your config(s) as attachments? Then I can try to build them at my end. If you want to try building my configs they are in my github linked a few posts up. What hardware are you using BTW?

Do you have basic instructions for compiling/building in esphome? Looks like the pioarduino release package for 6.8.1 isn’t available and seems like the LVGL components causes errors on the current stable release of esphome.

EDIT - fully working - this is great!

Using a 3D printed mount which hides the wires and I soldered 5V+GND directly to the unused pads on the breakout board so I can avoid the visible USB cable sticking out the side.

@wineds is there a way to ignore the first touch/rotate input when the backlight is off? I have an automation to turn off the lights after 60 seconds but the first click jumps into the menu which is a bit awkward. Thanks for the awesome code, I had very few issues adding it to ESPhome and compiling that way.

Have you tried launching the ESPhome docker container in a WSL image and using that? It built easily on HAOS and I imagine regular ESPhome is the same story if you are on Linux instead of Windows.

Thanks but its @kto (and @veli) you really need to be thanking as you appear to be using his code on your rotary display. My code is for the waveshare 2.1 touch display.

No I haven’t. I use a terminal window on linux as per here : Installing ESPHome Manually - ESPHome - Smart Home Made Simple

It builds quickly on a modern laptop and works out of the box.

Oops! Got my links confused!

Thanks @veli and @kto - any tips on using the first input event purely to enable the backlight?

Yeah, it’s so much easier on Linux than trying to compile code under Windows.

Figured out how to ignore the first press/rotate if the backlight is off IMHO this makes it a nicer experience if using a timeout to turn off backlight:

binary_sensor:
  ### CENTRE PUSH BUTTON
  - platform: gpio
    name: Button
    id: push_button
    icon: mdi:circle-outline
    internal: true
    pin:
      number: 3
      mode: INPUT
      inverted: true
      ignore_strapping_warning: true
    on_press: 
      # - script.execute:
      #     id: vibrate
      #     length_ms: 10
      - light.turn_on: backlight
      - if:
          condition:
            and:
              - lvgl.is_paused
              - light.is_on: backlight
          then:
            - lvgl.resume:
            - lvgl.widget.redraw:
      - if:
          condition:
            and:
              - light.is_on: backlight
              # only when on the main page
              - lambda: 'return strcmp(id(active_lvgl_page).c_str(), "main_page") == 0;'
          then:
            - script.execute: goto_menu_page

  - platform: status
    name: Status

button:
- platform: restart
  name: Restart
  disabled_by_default: True

sensor:
  - platform: uptime
    name: Uptime
    disabled_by_default: True

  ### ROTARY ENCODER
  - platform: rotary_encoder
    name: Rotary Encoder
    id: rotary
    icon: mdi:cached
    internal: true
    pin_a:
      number: 6
      inverted: true
      mode: INPUT
    pin_b:
      number: 5
      inverted: true
      mode: INPUT
    on_value:
      - light.turn_on: backlight
      - if:
          condition:
            and:
              - light.is_on: backlight
              # turn ON when when on main page and OFF
              - lambda: 'return strcmp(id(active_lvgl_page).c_str(), "main_page") == 0 && strcmp(id(hvac_mode).state.c_str(), "off") == 0;'
          then:
            - homeassistant.action:
                action: climate.turn_on
                data:
                  entity_id: ${climate_entity}
      - if:
          condition: 
            and:
              - light.is_on: backlight
              - lvgl.is_paused
          then:
            - lvgl.resume:
            - lvgl.widget.redraw:
    on_clockwise:
      - if:
          condition:
            and:
              - light.is_on: backlight
              # set temp++ when on main page and not OFF or FAN
              - lambda: 'return strcmp(id(active_lvgl_page).c_str(), "main_page") == 0 && strcmp(id(hvac_mode).state.c_str(), "off") != 0 && strcmp(id(hvac_mode).state.c_str(), "fan_only") != 0;'
          then:
            - homeassistant.action:
                action: climate.set_temperature
                data:
                  entity_id: ${climate_entity}
                  temperature: !lambda return id(set_temperature).state + 0.5;
    on_anticlockwise:
      - if:
          condition:
            and:
              - light.is_on: backlight
              # set temp-- when on main page and not OFF or FAN
              - lambda: 'return strcmp(id(active_lvgl_page).c_str(), "main_page") == 0 && strcmp(id(hvac_mode).state.c_str(), "off") != 0 && strcmp(id(hvac_mode).state.c_str(), "fan_only") != 0;'
          then:
            - homeassistant.action:
                action: climate.set_temperature
                data:
                  entity_id: ${climate_entity}
                  temperature: !lambda return id(set_temperature).state - 0.5;

Now I just have to figure out how to set a screen-off timer on the device itself without relying on an external automation.

Generally speaking the correct way to do that is wakeup on release, not press. That way if LVGL is paused the press that happens before the release is ignored.

2 Likes