Installing ESPhome on GEEKMAGIC Smart Weather Clock (smalltv/pro)

That did not work, i added the firmware from ESPHome and called it firmware.ota.bin and it did not flash the new firmware i created

Okay so you shouldn’t be renaming a file. When you compile through the command line one of the files produced is named firmware.ota.bin. That is the one I flashed using the factory web interface, then it worked.

Either way, if you can figure out the pinout of those pins on the left side of your picture you should be able to flash via serial the normal way.

I received this version also and have it working—sort of. For a start could not get the code to work for 240x240, had to reduce it to 235 x 235.
Flashed the CS-32E as a ESP32
My code for display

Display:
  - platform: ili9xxx
    id: lcd_display
    model: st7789v
    spi_id: spihwd
    data_rate: 40MHz #oringal device uses 20mhz - 40 is default and works - does not work at 80mhz
    #cs_pin: GPIO03 #CS pin is connected to gnd I believe
    dc_pin: GPIO02
    reset_pin: GPIO04
    spi_mode: MODE3   #since no cs pin default is mode0
    dimensions:
      width: 235
      height: 235
      offset_height: 0
      offset_width: 0
    invert_colors: true

So i am using ESPHome through Home Assistant and am not sure what i need flash to the device. @Dingo Would you be willing to post the whole yaml you used for yours?

Intially I used Home Assistant - ESPhomebuilder, create new device. I will post the code i used…I still have some issues with sizing but trying to fix these, but its a start

esphome:
  name: energytv
  friendly_name: EnergyTV

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:
  level: VERBOSE
  logs:
    #lvgl.component: VERBOSE
    light.component: ERROR
    light.output: ERROR
    number.component: ERROR
    ledc.output: ERROR
    template.number: ERROR

  baud_rate: 0 #no hardware serial connected to USB - unless using the internal programming header

# Enable Home Assistant API
api:
  encryption:
    key: "mine"

ota:
  - platform: esphome
    password: "mine" 

wifi:
  ssid: IOT
  password: mine

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Geekmagic-Hilo Fallback Hotspot"
    password: "mine"

captive_portal:

external_components:
 - source: github://clydebarrow/esphome@lvgl
   components: [ lvgl ]

# Define a PWM output on the ESP32
output:
  - platform: ledc
    pin: GPIO25
    inverted: True
    id: backlight_pwm

# Define a monochromatic, dimmable light for the backlight
light:
  - platform: monochromatic
    output: backlight_pwm
    name: "Display Backlight"
    id: back_light
    restore_mode: ALWAYS_ON

esp32_touch:
 # setup_mode: true

sensor:
  - platform: homeassistant
    entity_id: sensor.shellyem_98cdac1f0b5f_channel_1_voltage
    id: voltage
    internal: true
    state_class: measurement
    unit_of_measurement: Volts

  - platform: homeassistant
    entity_id: sensor.shellyem_98cdac1f0b5f_channel_1_power
    id: power
    internal: true
    state_class: measurement
    unit_of_measurement: Watts

  - platform: homeassistant
    entity_id: sensor.shellyem_98cdac1f0b5f_channel_2_power
    id: solarpower
    internal: true
    state_class: measurement
    unit_of_measurement: Watts




spi:
  clk_pin: GPIO18
  mosi_pin: GPIO23
  interface: hardware
  id: spihwd

color:
  - id: my_red
    red: 100%
    green: 0%
    blue: 0%
  - id: my_orange
    red: 100%
    green: 50%
    blue: 0%
  - id: my_yellow
    red: 100%
    green: 100%
    blue: 0%
  - id: my_green
    red: 0%
    green: 100%
    blue: 0%
  - id: my_blue
    red: 0%
    green: 0%
    blue: 100%
  - id: my_teal
    red: 0%
    green: 100%
    blue: 100%
  - id: my_gray
    red: 70%
    green: 70%
    blue: 70%
  - id: my_white
    red: 100%
    green: 100%
    blue: 100%
  - id: my_black
    red: 0%
    green: 0%
    blue: 0%

font:
  - file: "gfonts://Roboto"
    id: font_48
    size: 48
  - file: "gfonts://Roboto"
    id: font_36
    size: 36
  - file: "gfonts://Roboto"
    id: font_24
    size: 24
  - file: "gfonts://Roboto"
    id: font_12
    size: 12
  - file: "gfonts://Roboto"
    id: font_20
    size: 20

display:
  - platform: ili9xxx
    id: lcd_display
    model: st7789v
    spi_id: spihwd
    data_rate: 40MHz #oringal device uses 20mhz - 40 is default and works - does not work at 80mhz
    #cs_pin: GPIO03 #CS pin is connected to gnd I believe
    dc_pin: GPIO02
    reset_pin: GPIO04
    spi_mode: MODE3   #since no cs pin default is mode0
    dimensions:
      width: 235
      height: 235
      offset_height: 0
      offset_width: 0
    invert_colors: true
    #update_interval: never
    #auto_clear_enabled: false
    lambda: |-
      it.rectangle(0,  0, it.get_width(), it.get_height(), id(my_blue));
      it.rectangle(0, 20, it.get_width(), it.get_height(), id(my_blue));   // header bar
      
      //it.print(5, 5, id(font_12), id(my_yellow), TextAlign::TOP_LEFT, "ESPHome");
      it.strftime(5, 5, id(font_12), id(my_yellow), TextAlign::TOP_LEFT, "%H:%M:%S", id(time_comp).now());
      
      it.print((it.get_width() / 2), (240 / 5.5) * 1 - 8, id(font_20), id(my_yellow), TextAlign::CENTER, "Voltage");
      it.printf((it.get_width() / 2), (240 / 4) * 1 + 9, id(font_36), id(my_yellow), TextAlign::CENTER, "%.1fVac",id(voltage).state);
      
    
      it.print((it.get_width() / 2), (240 / 4.5) * 2 - 8, id(font_20), id(my_green), TextAlign::CENTER, "Grid Power");
      it.printf((it.get_width() / 2), (240 / 4) * 2 + 9, id(font_36), id(my_green), TextAlign::CENTER, "%.1fW",id(power).state);
     
      
      it.print((it.get_width() / 2), (240 / 4.2) * 3 - 8, id(font_20), id(my_orange), TextAlign::CENTER, "Solar Power");
      it.printf((it.get_width() / 2), (240 / 4) * 3 + 9, id(font_36), id(my_orange), TextAlign::CENTER, "%.1fW",id(solarpower).state);
      
      

     
      

     
      

time:
  - platform: sntp
    id: time_comp

4 Likes

That worked!!! It is giving me errors when it says mine??? im just kidding. Thanks again @Dingo

I was able to get everything working on a PRO variant (ESP32E) using LVGL and no external ESPHome components. I’m using ESPHome 2025.3.2.

The full 240x240 resolution is also working.

This example boots up with the ESPHome logo and a spinner (taken from the ESPHome LVGL documentation) and then shows a clock with date and time (taken from ESPHome documentation).

Seems to work very well! Thanks for everyone’s help.

esphome:
  name: esphome-web-8af3ac
  friendly_name: SmallTV Pro
  min_version: 2024.11.0
  name_add_mac_suffix: false
  on_boot:
    - delay: 5s
    - lvgl.widget.hide: boot_screen


esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:
  baud_rate: 0

# Enable Home Assistant API
api:

ota:
  - platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

# Define a PWM output on the ESP32
output:
  - platform: ledc
    pin: GPIO25
    inverted: True
    id: backlight_pwm

# Define a monochromatic, dimmable light for the backlight
light:
  - platform: monochromatic
    output: backlight_pwm
    name: "Display Backlight"
    id: back_light
    restore_mode: ALWAYS_ON

esp32_touch:
 # setup_mode: true

binary_sensor:
  - platform: esp32_touch
    name: "Touch Button"
    pin: GPIO32
    threshold: 1250

spi:
  clk_pin: GPIO18
  mosi_pin: GPIO23
  interface: hardware
  id: spihwd

display:
  - platform: ili9xxx
    id: lcd_display
    model: st7789v
    spi_id: spihwd
    data_rate: 40MHz
    dc_pin: GPIO02
    reset_pin: GPIO04
    spi_mode: MODE3
    dimensions:
      width: 240
      height: 240
      offset_height: 0
      offset_width: 0
    invert_colors: true
    auto_clear_enabled: false
    update_interval: never
     
time:
  - platform: sntp
    id: time_comp
    on_time_sync:
      - script.execute: time_update
    on_time:
      - minutes: '*'
        seconds: 0
        then:
          - script.execute: time_update

image:
  - file: https://esphome.io/_static/favicon-512x512.png
    id: boot_logo
    resize: 200x200
    type: RGB565
    transparency: alpha_channel

lvgl:
  buffer_size: 25%
  top_layer:
    widgets:
      - label:
          text: "\uF1EB"
          id: lbl_hastatus
          hidden: true
          align: top_right
          x: -2
          y: 7
          text_align: right
          text_color: 0xFFFFFF
      - obj:
          id: boot_screen
          x: 0
          y: 0
          width: 100%
          height: 100%
          bg_color: 0xffffff
          bg_opa: COVER
          radius: 0
          pad_all: 0
          border_width: 0
          widgets:
            - image:
                align: CENTER
                src: boot_logo
                y: -40
            - spinner:
                align: CENTER
                y: 75
                height: 50
                width: 50
                spin_time: 1s
                arc_length: 60deg
                arc_width: 8
                indicator:
                  arc_color: 0x18bcf2
                  arc_width: 8
          on_press:
            - lvgl.widget.hide: boot_screen
  pages:
    - id: clock_page
      widgets:
        - obj: # clock container
            height: SIZE_CONTENT
            width: 240
            align: CENTER
            pad_all: 0
            border_width: 0
            bg_color: 0xFFFFFF
            widgets:
              - meter: # clock face
                  height: 220
                  width: 220
                  align: CENTER
                  bg_opa: TRANSP
                  border_width: 0
                  text_color: 0x000000
                  scales:
                    - range_from: 0 # minutes scale
                      range_to: 60
                      angle_range: 360
                      rotation: 270
                      ticks:
                        width: 1
                        count: 61
                        length: 10
                        color: 0x000000
                      indicators:
                        - line:
                            id: minute_hand
                            width: 3
                            color: 0xa6a6a6
                            r_mod: -4
                            value: 0
                    - range_from: 1 # hours scale for labels
                      range_to: 12
                      angle_range: 330
                      rotation: 300
                      ticks:
                        width: 1
                        count: 12
                        length: 1
                        major:
                          stride: 1
                          width: 4
                          length: 10
                          color: 0xC0C0C0
                          label_gap: 12
                    - range_from: 0 # hi-res hours scale for hand
                      range_to: 720
                      angle_range: 360
                      rotation: 270
                      ticks:
                        count: 0
                      indicators:
                        - line:
                            id: hour_hand
                            width: 5
                            color: 0xa6a6a6
                            r_mod: -30
                            value: 0
              - label:
                  align: CENTER
                  id: day_label
                  y: -30
              - label:
                  align: CENTER
                  id: date_label
                  y: 30

script:
  - id: time_update
    then:
      - lvgl.indicator.update:
          id: minute_hand
          value: !lambda |-
            return id(time_comp).now().minute;
      - lvgl.indicator.update:
          id: hour_hand
          value: !lambda |-
            auto now = id(time_comp).now();
            return std::fmod(now.hour, 12) * 60 + now.minute;
      - lvgl.label.update:
          id: date_label
          text: !lambda |-
            static const char * const mon_names[] = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"};
            static char date_buf[8];
            auto now = id(time_comp).now();
            snprintf(date_buf, sizeof(date_buf), "%s %2d", mon_names[now.month-1], now.day_of_month);
            return date_buf;
      - lvgl.label.update:
          id: day_label
          text: !lambda |-
            static const char * const day_names[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
            return day_names[id(time_comp).now().day_of_week - 1];
6 Likes

Thanks, this is awesome.

FYI on esphome version 2024.9.2 240x240 res. works just fine. Looks like problem lies in underlying libraries.

So I got a SmallTV-pro (ESP32), tried a few things, and most were quite hard for a beginner.

My findings so far:

  • Using lambda to render the display means that the dimensions need to have a value of 235. But there is a bright noisy stripe in the remaining are. Using a higher value makes the display crash. (Which can be annoying because it just freezes after an update. Only after a power cycle it’s going to be all noisy and therefore obviously faulty.)

This works:

display:
  - platform: ili9xxx
    id: lcd_display
    model: st7789v
    spi_id: spihwd
    data_rate: 40MHz
    dc_pin: GPIO02
    reset_pin: GPIO04
    spi_mode: MODE3
    dimensions:
      width: 235
      height: 235
      offset_height: 0
      offset_width: 0
    invert_colors: true
    #update_interval: never
    #auto_clear_enabled: false
  • LVGL can render the full resolution.

Working code:

display:
  - platform: ili9xxx
    id: lcd_display
    model: st7789v
    spi_id: spihwd
    data_rate: 40MHz
    dc_pin: GPIO02
    reset_pin: GPIO04
    spi_mode: MODE3
    dimensions:
      width: 240
      height: 240
      offset_height: 0
      offset_width: 0
    invert_colors: true
    auto_clear_enabled: false
    update_interval: never
  • Neither (free) ChatGPT nor (free) Copilot produce working code. ChatGPT is a better though.
  • Even ChatGPT is mostly useless when Home Assistant itself is concerned. It often relies on outdated mechanisms and won’t stop even if told so.

But I’ve managed to display the departure time of the next upcoming train for my commute. The goal is to have a full schedule for five or so connections.

I am a totally noob on esphome - has anybody of you a bin file for me, to update it via the build in webui of the geekmagic pro (esp32) variant, please? so I can add this device to my home asssitant instance via esphome

I would like to display information from the home assistant to this geekmagic pro clock

lovely, thanks for this fine example.

Tried first with the stv-ultra and texts, which was annoying due to lack of memory when changing things. When flashing again, had to go back first to the minimal firmware always.

Your lvgl approach looks much more promising

1 Like

You need to install it as a new device in the ESPHome Device Builder (install the add-on in your Home Assistant). The platform is ESP32.

After it is created you choose manual download and the legacy method.

The file you receive (*.bin) can be inserted in the update section of the SmallTV configuration website.

1 Like

Looks great! Glad it worked for you. I’ve added a couple more sensor screens and now use the touch button to change between pages. Seems to work very well.

Cool, could you share that button part?

I played around a bit for my needs - I added my PV + EV sensors, now trying to use some emojis which needs some fonts to be embedded first and maybe some colors.

esphome:
  name: esphome-web-8af3ac
  friendly_name: SmallTV Pro
  min_version: 2024.11.0
  name_add_mac_suffix: false

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:
  baud_rate: 0

# enable web server
web_server:
  version: 3

# Enable Home Assistant API
api:

ota:
  - platform: esphome

wifi:
  ssid: !secret wifi-ssid
  password: !secret wifi-password

# Define a PWM output on the ESP32
output:
  - platform: ledc
    pin: GPIO25
    inverted: True
    id: backlight_pwm

# Define a monochromatic, dimmable light for the backlight
light:
  - platform: monochromatic
    output: backlight_pwm
    name: "Display Backlight"
    id: back_light
    restore_mode: ALWAYS_ON

esp32_touch:
  # setup_mode: true

binary_sensor:
  - platform: esp32_touch
    name: "Touch Button"
    pin: GPIO32
    threshold: 1250

spi:
  clk_pin: GPIO18
  mosi_pin: GPIO23
  interface: hardware
  id: spihwd

display:
  - platform: ili9xxx
    id: lcd_display
    model: st7789v
    spi_id: spihwd
    data_rate: 40MHz
    dc_pin: GPIO02
    reset_pin: GPIO04
    spi_mode: MODE3
    dimensions:
      width: 240
      height: 240
      offset_height: 0
      offset_width: 0
    invert_colors: true
    auto_clear_enabled: false
    update_interval: never

lvgl:
  buffer_size: 25%
  widgets:
    # DC
    - label:
        id: power_label
        text: "Power: 0.00 Watt"
        x: 20 # Adjust position
        y: 20
        width: 200
        text_align: CENTER
    # Forecast
    - label:
        id: forecast_label
        text: "Forecast Today: 0.00 kW"
        x: 20 # Adjust position
        y: 40
        width: 200
        text_align: CENTER        
    # Home Battery    
    - label:
        id: battery_label
        text: "Home Battery: 0%"
        x: 20
        y: 80  # Position below power display
        width: 200
        text_align: CENTER
    - bar:
        id: battery_bar
        x: 20
        y: 100
        width: 200
        height: 20
        min_value: 0
        max_value: 100
        value: 0
    # Car 1 Battery (id_4_state_of_charge)
    - label:
        id: car1_label
        text: "Car 1: 0%"
        x: 10
        y: 140
        width: 100
        text_align: RIGHT
    - label:
        id: car1_rangelabel
        text: "Car 1:0km"
        x: 100
        y: 140
        width: 100
        text_align: RIGHT        
    - bar:
        id: car1_bar
        x: 10
        y: 160
        width: 200
        height: 15
        min_value: 0
        max_value: 100

    # Car 2 Battery (smart_batterie)
    - label:
        id: car2_label
        text: "Car 2: 0%"
        x: 10
        y: 180
        width: 100
        text_align: RIGHT
    - label:
        id: car2_rangelabel
        text: "Car 2: 0km"
        x: 100
        y: 180
        width: 100
        text_align: RIGHT
    - bar:
        id: car2_bar
        x: 10
        y: 200
        width: 200
        height: 15
        min_value: 0
        max_value: 100



sensor:
  # DC Power
  - platform: homeassistant # or your actual sensor platform
    id: total_dc_power
    unit_of_measurement: "Watt"    
    entity_id: sensor.total_dc_power # Replace with your HA entity
    on_value:
      - lvgl.label.update:
          id: power_label
          text: !lambda |-
            return ("Sun Power: " + to_string(static_cast<int>(x)) + " Watt").c_str();

  # Forecast
  - platform: homeassistant # or your actual sensor platform
    id: forecast_remaining
    entity_id: sensor.solcast_pv_forecast_prognose_verbleibende_leistung_heute # Replace with your HA entity
    on_value:
      - lvgl.label.update:
          id: forecast_label
          text: !lambda |-
            return ("Solcast remaining: " + to_string(static_cast<int>(x)) + " kWh").c_str();

  # Home Battery
  - platform: homeassistant
    id: evcc_battery_soc
    entity_id: sensor.evcc_battery_soc # Replace with actual HA entity
    unit_of_measurement: "%"
    on_value:
      - lvgl.label.update:
          id: battery_label
          text: !lambda |-
            return ("Home Battery: " + to_string(static_cast<int>(x)) + "%").c_str();
      - lvgl.bar.update:
          id: battery_bar
          value: !lambda |-
            return x;

  # Car 1 Battery
  - platform: homeassistant
    id: car1_soc
    entity_id: sensor.id_4_state_of_charge  # Replace with actual entity
    unit_of_measurement: "%"
    on_value:
      - lvgl.label.update:
          id: car1_label
          text: !lambda |-
            return ("ID4: " + to_string(static_cast<int>(x)) + "%").c_str();
      - lvgl.bar.update:
          id: car1_bar
          value: !lambda |-
            return x;

  # Car 1 Range
  - platform: homeassistant
    id: car1_range
    entity_id: sensor.id_4_range  # Replace with actual entity
    unit_of_measurement: "km"
    on_value:
      - lvgl.label.update:
          id: car1_rangelabel
          text: !lambda |-
            return (to_string(static_cast<int>(x)) + "km").c_str();


  # Car 2 Battery
  - platform: homeassistant
    id: car2_soc
    entity_id: sensor.smart_batterie  # Replace with actual entity
    unit_of_measurement: "%"
    on_value:
      - lvgl.label.update:
          id: car2_label
          text: !lambda |-
            return ("Smart #1: " + to_string(static_cast<int>(x)) + "%").c_str();
      - lvgl.bar.update:
          id: car2_bar
          value: !lambda |-
            return x;

  # Car 2 Range
  - platform: homeassistant
    id: car2_range
    entity_id: sensor.smart_reichweite  # Replace with actual entity
    unit_of_measurement: "km"
    on_value:
      - lvgl.label.update:
          id: car2_rangelabel
          text: !lambda |-
            return ( to_string(static_cast<int>(x)) + "km").c_str();

a online lvgl editor would come in handy, looking for one

4 Likes

Looks really good! EV’s and solar?? Jealous. We have EV’s just no solar (yet)…

The touch button control was done by tieing a binary_sensor to the esp32_touch platform and using an on_press event.

This block added to my original post will get it working.

This will rotate forward through all pages and wrap around automatically to the first one. I am likely going to add a press-and-hold to go backwards.

binary_sensor:
  - platform: esp32_touch
    name: "Touch Button"
    pin: GPIO32
    threshold: 1250
    on_press:
      then:
        - lvgl.page.next:
            animation: FADE_IN
            time: 300ms
3 Likes

I think there is a difference in the ESP module. Wheni tried to get your code working for the display, it didnt show errors but just showed a screen of white noise.

Thx AoSpades - you pointed me to the right direction :slight_smile: I have flashed the device and added it to ha successfully :slight_smile:

now starts the hard stuff, right? how to customize the screen :wink: I would like to show PV information - is it possible to show mqtt device information?

Are you able to share a photo of the module side of your board? There appear to be some modified and/or clones of the original geekmagic devices that might be different enough to not work with this config.