ESPHome Nest thermostat clone on cheap rotary display

I bought a rotary display on aliexpress, was pretty disappointed to find it did not have touch, the display was not full size, etc.

So it was in the electronics pile for a while, until I came up with the best I could with the awkward rectangular display inside a circular form factor.

Currently it’s not connected to a physical thermostat outlet, but a virtual HA device to get the sensor values.

Fun features

  1. Show a QR code if it’s not connected to wifi
    1.1 Show another QR code to add it to HA if not connected to API
  2. Climate state, temperature, current_temperature, etc
  3. Icons, mapped to preset_mode and state
  4. Colored background like Nest if heating, text ECO if in eco preset_mode

Actual device

Now I’m less disappointed :slight_smile: and think it can be usable. The rotary sensors are a little off, the pushbutton doesn’t work reliably, but it has a vibration motor inside which is fun.

It was :face_with_symbols_over_mouth: to find out what pin does what from this schematic, maybe haven’t figured it all out yet.

Will post ESPHome code soon, have some bugs to iron out.


Can be skinned pretty easily, so one can choose Nest or Apple Home styles:


Bummer- I am looking for exactly that display, but Ali Express responds with: “Sorry, this item is no longer available!”

Try this

If i was buying a new one and knew what I know now, I’d get a full width display and not a esp32 solo chip, so, either one:


More theme possibilities:

  1. Apple (Home app)
  2. Nest
  3. Google Home (app)

Looking forward to your code :slight_smile:

Thanks. One is on order.

OK, got it in the mail today. I would have appreciated it if AliExpress ever included any data sheets or user instructions.

Can you please share your code so far, and how to flash the knob?

There is a schematic above. Which one exactly did you buy?

The first one on the page suggested by @RoadkillUK RoadkillUK
The schematic is the main board, not the interface board. I am looking forward to seeing the YAML code and tips for how to flash the unit.

I assume this one then, which actually has two models, which one did you get?

That is what I ordered, but this is what I received:


Interesting! That back piece doesn’t seem to be pictured on the advert! Does it come off easily? I can’t find the similar thing I bought a year or more ago! (Never got it going, it is (somewhere) in the to do pile.

It plugs into the back. I haven’t opened it up yet.

That looks like the connector on the back of mine.

From the advert on ali, it appears to be this:

I assume the smaller white connector plugs into the mainboard and the black connector is what you see on the back of the device. This may enable you to trace what is connected to where on that green sub-board.

What happens when you plug the USB into a computer with a data cable? Is it showing as a serial device? Or perhaps the USB only supplies power.

Just power. And a demo program in Chinese.

So are Rx and Tx connected to the USB port? (Assuming you have a meter to check :slight_smile: )

There are few traces going to the USB-C connector, and those two vias in the center go to the USB jumpers JP1 and JP2, bot open.

I haven’t opened it to see where the many-pin connector goes.

There is also a short ‘debug’ cable and board:

So it looks like flashing the module should be straightforward. Now I just need the code…

I didn’t get the debug cable - just open the screws on the circle and you find TX RX IO0 and GND pins exposed.

here’s what i found:

  board: esp32-s2-saola-1
    type: arduino
    version: 2.0.3
    platform_version: 5.0.0

  - platform: ledc
    id: ${device_name}_backlight_pwm
    pin: GPIO35
    frequency: 19531Hz
    zero_means_zero: true

  clk_pin: GPIO14 # sck : 14
  mosi_pin: GPIO33 # sda : 33

display: # 240 x 320
  - platform: st7789v
    id: ${device_name}_display
    model: Custom
    width: 240
    height: 320
    offset_height: 0
    offset_width: 0
    eightbitcolor: true # OR IT BREAKS
###    backlight_pin: GPIO35 ## breaks color
    update_interval: 20s
    cs_pin: GPIO34
    dc_pin: GPIO13
    reset_pin: GPIO21

  - platform: gpio
      number: 18
    name: Motor
    id: ${device_name}_motor
    entity_category: diagnostic
    icon: mdi:vibrate
        - delay: 0.1s
        - switch.turn_off: ${device_name}_motor
    internal: true

  - platform: monochromatic
    id: ${device_name}_backlight
    output: ${device_name}_backlight_pwm
    icon: mdi:brightness-percent
    name: Backlight
    default_transition_length: 500ms
    restore_mode: ALWAYS_ON

## will not work as rotary_encoder sensor because it bounces all over...
  - platform: gpio
      number: GPIO17 
    name: TEST left # is okay
    id: ${device_name}_gpio17_debounced

  - platform: gpio
      number: GPIO16
    name: TEST right
    id: ${device_name}_gpio16_debounced
      - delayed_on: 200ms
      - delayed_off: 500ms
      - lambda: |-
          if (id(${device_name}_gpio17_debounced).state) {
            return x;
          } else {
            return {};
        - lambda: |-
            if (id(${device_name}_gpio17_debounced).state) {
            } else {
        - script.execute: vibrate

## tried all delays, still cant get it to work reliably 
  - platform: gpio
      number: GPIO2
    name: Button
    id: ${device_name}_button
    icon: mdi:circle-outline
      - delayed_on: 100ms 
      - delayed_off: 100ms 
      - delayed_on_off: 550ms 
      - lambda: |-      
          ESP_LOGW("button", "pressed at %lu", millis());
      - lambda: |-      
          ESP_LOGW("button", "release at %lu", millis());

Thanks for the code. I did open it up and found this:

Putting it back together was a bit of a challenge: getting the cable plugged in and not losing a spring.

Plugging it all in:

This did connect a UART and the PC reported it as COM33. Running Termite and pressing the reset button gave me this:

Build:Oct 25 2019
rst:0x1 (POWERON),boot:0x8 (SPI_FAST_FLASH_BOOT)
mode:DIO, clock div:1
entry 0x4004c21c
[1B][0;32mI (21) boot: ESP-IDF v4.3.1-29-ge493a4c30e-dirty 2nd stage bootloader[1B][0m
[1B][0;32mI (21) boot: compile time 16:57:12[1B][0m
[1B][0;32mI (22) boot: chip revision: 0[1B][0m
[1B][0;32mI (26) qio_mode: Enabling default flash chip QIO[1B][0m
[1B][0;32mI (31) boot.esp32s2: SPI Speed      : 80MHz[1B][0m
[1B][0;32mI (36) boot.esp32s2: SPI Mode       : QIO[1B][0m
[1B][0;32mI (40) boot.esp32s2: SPI Flash Size : 4MB[1B][0m
[1B][0;32mI (45) boot: Enabling RNG early entropy source...[1B][0m
[1B][0;32mI (51) boot: Partition Table:[1B][0m
[1B][0;32mI (54) boot: ## Label            Usage          Type ST Offset   Length[1B][0m
[1B][0;32mI (61) boot:  0 nvs              WiFi data        01 02 00009000 00006000[1B][0m
[1B][0;32mI (69) boot:  1 phy_init         RF data          01 01 0000f000 00001000[1B][0m
[1B][0;32mI (76) boot:  2 factory          factory app      00 00 00010000 00300000[1B][0m
[1B][0;32mI (84) boot: End of partition table[1B][0m
[1B][0;32mI (88) esp_image: segment 0: paddr=00010020 vaddr=3f000020 size=a9294h (692884) map[1B][0m
[1B][0;32mI (215) esp_image: segment 1: paddr=000b92bc vaddr=3ffc92b0 size=04098h ( 16536) load[1B][0m
[1B][0;32mI (219) esp_image: segment 2: paddr=000bd35c vaddr=40024000 size=02cbch ( 11452) load[1B][0m
[1B][0;32mI (224) esp_image: segment 3: paddr=000c0020 vaddr=40080020 size=bab80h (764800) map[1B][0m
[1B][0;32mI (361) esp_image: segment 4: paddr=0017aba8 vaddr=40026cbc size=125f4h ( 75252) load[1B][0m
[1B][0;32mI (378) esp_image: segment 5: paddr=0018d1a4 vaddr=50000000 size=00010h (    16) load[1B][0m
[1B][0;32mI (388) boot: Loaded app from partition at offset 0x10000[1B][0m
[1B][0;32mI (388) boot: Disabling RNG early entropy source...[1B][0m
[1B][0;32mI (400) cache: Instruction cache 	: size 8KB, 4Ways, cache line size 32Byte[1B][0m
[1B][0;32mI (400) cpu_start: Pro cpu up.[1B][0m
[1B][0;32mI (414) cpu_start: Pro cpu start user code[1B][0m
[1B][0;32mI (414) cpu_start: cpu freq: 240000000[1B][0m
[1B][0;32mI (414) cpu_start: Application information:[1B][0m
[1B][0;32mI (419) cpu_start: Project name:     knobscreen2.4[1B][0m
[1B][0;32mI (424) cpu_start: App version:      4676002-dirty[1B][0m
[1B][0;32mI (429) cpu_start: Compile time:     Jan  8 2022 18:14:16[1B][0m

So, we’re making progress…