ESPHome Nest thermostat clone on cheap rotary display

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:

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

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

spi:
  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

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

light:
  - 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

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

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

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

ESP-ROM:esp32s2-rc4-20191025
Build:Oct 25 2019
rst:0x1 (POWERON),boot:0x8 (SPI_FAST_FLASH_BOOT)
SPIWP:0xee
mode:DIO, clock div:1
load:0x3ffe6100,len:0x19c4
load:0x4004c000,len:0xc9c
load:0x40050000,len:0x2ce0
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…

Did you hold boot (or bridge IO0 to GND) while connecting the cable to get into flashing mode? I just soldered onto the exposed pads to connect my usb. This way I could flash it with https://web.esphome.io/

I haven’t flashed anything to it yet. I want to find how to change the graphics first.

I did find an online tool for designing the graphics. At least I think it is the right LCD for this knob. I haven’t tried using it yet.

Also found this YouTube tutorial.

Yeah that’s their crap design tool. Really awful.

Wrong video I think. This one is what you wanted to point to isn’t it?

Buuuuut we are in the esphome section of the forum, so not quite right.

Any chance of your code my friend?

True, but if I can figure out how to flash new code and graphics, connecting to ESPHome shouldn’t be too difficult.

Same as flashing any other esphome code I guess. You have a serial connection, use it.

I’ve probably flashed a hundred ESP devices. My stumbling block is which graphics library to use for the display.

If you want ESPHome, then i dont think theres an alternative to display

@nickrout - pasted the hardware that needs to be worked out, display screens are from Figma, nothing finished in ESPHome yet, but nothing too hard:


font:
  - file: 'fonts/Roboto-Regular.ttf' 
    glyphs: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ°.:/+- ' #ABCDEFGHIJKLMNOPQRSTUVWXYZ°
    id: regular
    size: 24
  - file: 'fonts/Roboto-Regular.ttf' 
    glyphs: ' -0123456789°Cecohi' #C
    id: huge
    size: 80
  - file: 'fonts/Roboto-Regular.ttf' 
    glyphs: ' -0123456789°Cecohi' #C
    id: medium
    size: 60
  - file: "fonts/fa-light.ttf"
    glyphs: "\uf06c\uf7ae\uf79a\uf06d\uf1eb\ue137\uf011\ue487\uf293\ue00d\uf015\ue1b0\uf624\ue3b0\ue1a7\uf2dc\ue004\uf775\uf2f1\uf863\uf750\uf017"


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
    on_page_change: 
      then: 
        - light.turn_on: ${device_name}_backlight

    pages:
    - id: page_boot
      lambda: |-
        int w = it.get_width();
        int h = it.get_height();
        auto icon = "\ue1a7"; // hand-wave //
        auto color = id(gray);
        it.print(w/2, h/2 , id(huge), random_color, TextAlign::CENTER, "hi");
        it.print(w/2, w, id(symbol), random_color, TextAlign::CENTER, icon);
        it.print(w/2, h - 20, id(regular), random_color, TextAlign::CENTER, "nESP");        

    - id: page_qr_wifi
      lambda: |-
        int w = it.get_width();
        int h = it.get_height();
        auto icon = "\uf1eb"; // \uf293 = bluetooth // \uf1eb = wifi
        auto color = id(gray);
        it.qr_code(w/3.5, h/4, id(qr_hotspot), random_color, 4);
        it.print(w/2, w, id(symbol), random_color, TextAlign::CENTER, icon);
        it.printf(w/2, h - 20, id(regular), random_color, TextAlign::CENTER, "nESP" );

    - id: page_climate
      lambda: |-
        // canvas
        int w = it.get_width();
        int h = it.get_height();
        int h2 = h / 2;
        int w2 = w / 2;
        int yheader = h / 3;
        int yfooter = h - 20;
        int radius = w2;
  
        // font sizes
        auto main_font = id(huge);
  
        // text values
        std::string huge_text = "x";
        auto get_mode = id(hvac_mode).state;
        auto get_hvac_action = id(hvac_action).state;
        auto get_preset_mode = id(preset_mode).state;
  
        // bools
        auto dual = false;
  
        // numeric
        float get_target_temp_low = id(target_temp_low).state;
        float get_target_temp_high = id(target_temp_high).state;
        float get_target_temperature = id(target_temperature).state; 

        // check for dual mode
        if (isnan(get_target_temperature) ){
          dual = true;
          get_target_temperature = round((get_target_temp_low + get_target_temp_high) / 2);
        }
  
        float get_current_temperature = id(current_temperature).state;
        float minTemp = 15.0;
        float maxTemp = 30.0;
  
        // COLORS
        auto color_neutral = id(graydark);
        auto color_main = id(graydark);
        auto color_accent = color_main;
        auto color_background = id(black);
        auto linecolor = id(graydarker);
  
        // ICON
        auto icon = "\uf624";
  
        // hvac_mode (state)
        if ( get_mode == "heat" ){
          color_main = id(mode_heat);
          icon = "\uf06d"; // fire
        }
        else if (get_mode == "cool") { 
          color_main = id(mode_cool);
          icon = "\uf2dc"; // snowflake
        }
        else if (get_mode == "dry") { 
          color_main = id(mode_dry);
          icon = "\uf750"; // droplet-percent
        }
        else if (get_mode == "fan_only") { 
          color_main = id(mode_fan);
          icon = "\uf863"; // fan
        }
        else if (get_mode == "heat_cool") { 
          color_main = id(mode_heat_cool);
          icon = "\uf2f1"; // rotate          
        }        

        // heat_cool hvac_action options: idle, cooling, heating
        if (get_hvac_action == "idle") { 
          if (get_mode == "heat_cool"){
            color_main = id(mode_heat_cool);
          }          
          else if (get_mode == "heat"){
            color_main = id(mode_heat);
          }          
          else if (get_mode == "cool"){
            color_main = id(mode_cool);
          }          
          else{
            color_main = id(mode_idle);
            icon = "\uf017"; // clock            
          }
          icon = "\uf017"; // clock            
          color_accent = id(mode_idle);
        }

        if ( (get_mode == "off") || (get_hvac_action == "off") ){ 
          color_main = id(graydarker);
          color_accent = id(graydark);
          linecolor = id(graydarker);
          icon = "\uf011"; // power-off
        }
  
        // Draw filled circle if not off or idle
        if ( (get_hvac_action == "heating") || (get_hvac_action == "cooling") || (get_hvac_action == "drying") || (get_hvac_action == "fan" ) ) { 
          // stripeLength = stripeLength + w / 40;
          radius = radius - w / 40;
          it.filled_circle(w2, h2, w2, color_main);
          linecolor = id(white);
          color_accent = color_main;
          it.print(w2, yheader, id(regular), color_background, TextAlign::CENTER, get_hvac_action.c_str() );
        }        

        if (get_preset_mode == "eco") {
          huge_text = "eco";
          color_main = id(mode_eco);
          } else  {
            char buffer[32];
            float value = round(get_target_temperature);
            if (value != 0 && !isnan(value) ){
              snprintf(buffer, sizeof(buffer), "%.0f", value); // Convert float to a C-style string
              huge_text = buffer;
            }                
          }
  
          if ( dual ){
            main_font = id(medium);
            float value_low = round(get_target_temp_low);
            float value_high = round(get_target_temp_high);
            std::string separator = " - ";
            huge_text = std::to_string((int)value_low) + separator + std::to_string((int)value_high);
          }  
          else {
            float value = round(get_target_temperature);
            if (value != 0 && !isnan(value) ){
              huge_text = std::to_string((int)value);
            }                
          }                    
  
  
          // Draw current temperature at bottom
          it.printf(w2, yfooter, id(regular), color_neutral, TextAlign::CENTER, "%.0f", get_current_temperature );
  
          // dual gauge mode:
          // it.printf(w2, yfooter, id(regular), color_main, TextAlign::CENTER, "%.0f - %.0f", get_target_temp_low, get_target_temp_high);
  
          // invert colors for filled background
          if ( (get_hvac_action == "heating") || (get_hvac_action == "cooling") || (get_hvac_action == "drying") || (get_hvac_action == "fan" ) ) { 
            auto hold = color_accent;
            color_accent = color_background;
            color_main = color_background;
            color_background = hold;
          }
  
          // Draw icon 
          it.print(w2, w, id(symbol), color_main, TextAlign::CENTER, icon);
  
          // draw set at center
          it.print(w2, h2 , main_font, color_accent, TextAlign::CENTER, huge_text.c_str() );
  

Thanks for the homework assignment.

Happy to help, together we might come to a final solution faster - i can finalize the display if someone helps figure out how to make the gpio sensors work reliably… I dont think you can use any other graphics “library” on st7789v… together with esphome.

Thank you!

It seems to me that openhasp would be ideal for this. Although it doesn’t have a touch screen, the interface of a rotary plus a push button could work with openhasp.

Did anybody have a look at this already?

https://aliexpress.com/item/1005004882568128.html

Information is a little bit sparse but it seems to be a commercial version of Smartknob

I just recently purchased one of these and am looking to do the same thing and would love to help in the development of this.

1 Like

Have a go, my enthusiasm tanked when the basic left-right-push sensors were out of whack.

2 Likes

It’s a dead link.

Works for me