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

Can I use this code also for the ESP8266? By just changing ESP32 to ESP8266?
Or will that not work?

Also can I provide mqtt username and password?
Because I do have that on my mqtt server.

Not exactly, but you can flash a ESP8266, many of us have done it, see my post here of the code I’m using:

Yes this is actually for Esp32 and will work only there. I have made a version for my other esp8266 based SmallTV as well, will post it later. It comes with a few quirks though and could use some optimisation in terms of display refresh.

Setting the user/pw should work like this:

# Example configuration entry
mqtt:
  broker: <broker>
  username: <user>
  password: !secret mqtt_password

see MQTT Client Component - ESPHome - Smart Home Made Simple

st7789v driver has been deprecated for a while in favor of mipi_spi. This has been mentioned several times over the thread, but unfortunately it is too long to read and find the relevant post…

Just update your display section as follows, while keeping your pages/lambda code. You can also remove all external_components. Key there is the buffer_size, which is a new feature that no longe requires the whole picture to reside on ESP8266 RAM.

  - id: my_display
    platform: mipi_spi
    model: ST7789V
    spi_id: spihwd
    dimensions:
      height: 240
      width: 240
      offset_height: 0
      offset_width: 0
    buffer_size: 12.5%
    invert_colors: true
    dc_pin: GPIO00
    reset_pin: GPIO02
    color_depth: 16
    update_interval: never
    spi_mode: mode3
    data_rate: 40000000
    auto_clear_enabled: False

That did it, thanks!

This is the updated version of the yaml for the ESP8266 using the most adequate driver (Thanks to @lhartmann)

esphome:
  name: smalltv
  friendly_name: SmallTV

esp8266:
  board: esp12e

# Enable logging
# logger:
  # level: DEBUG

# WiFi configuration
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: "SmallTV Fallback"
    password: "secretPW"

captive_portal:

# OTA updates
ota:
  - platform: esphome
    password: !secret ota_password

# MQTT broker configuration (no user/password)
mqtt:
  broker: <broker hostname>
  port: 1883
  on_json_message:
    - topic: <topic>
      then:
        - lambda: |-
            if (!x["items"].is<JsonArrayConst>()) return;
            JsonArrayConst items = x["items"].as<JsonArrayConst>();
            int count = items.size();
            if (count > 10) count = 10;

            id(item_count) = count;

            for (int i = 0; i < count; i++) {
              JsonObjectConst item = items[i].as<JsonObjectConst>();
              std::string label = item["label"].as<std::string>();
              std::string value = item["value"].as<std::string>();
              std::string rgb = item["value_rgb"].as<std::string>();
              id(item_labels)[i] = label;
              id(item_values)[i] = value;
              id(item_colors)[i] = rgb;
            }

            for (int i = count; i < 10; i++) {
              id(item_labels)[i] = "";
              id(item_values)[i] = "";
              id(item_colors)[i] = "FFFFFF";
            }
        - component.update: my_display

# Global variables to store parsed data
globals:
  - id: item_count
    type: int
    restore_value: no
    initial_value: "0"
  - id: item_labels
    type: std::string[10]
    restore_value: no
  - id: item_values
    type: std::string[10]
    restore_value: no
  - id: item_colors
    type: std::string[10]
    restore_value: no

# Fonts
font:
  - file: "gfonts://Roboto"
    id: font_header
    size: 16
  - file: "gfonts://Roboto"
    id: font_label
    size: 14
  - file: "gfonts://Roboto"
    id: font_value
    size: 16

color:
  - id: color_bg
    hex: "0A0A1A"
  - id: color_header_bg
    hex: "1A1A2E"
  - id: color_header_text
    hex: "00BFFF"
  - id: color_row_bg
    hex: "16213E"
  - id: color_row_border
    hex: "0F3460"
  - id: color_label_text
    hex: "A0A0C0"
  - id: color_default_value
    hex: "00FF88"

output:
  - platform: esp8266_pwm
    pin: GPIO05
    frequency: 20 Hz
    id: pwm_output

light:
  - platform: monochromatic
    output: pwm_output
    name: "Backlight"

spi:
  clk_pin: GPIO14
  mosi_pin: GPIO13
  interface: hardware
  id: spihwd

display:
  - id: my_display
    platform: mipi_spi
    model: ST7789V
    spi_id: spihwd
    dimensions:
      height: 240
      width: 240
      offset_height: 0
      offset_width: 0
    buffer_size: 12.5%
    invert_colors: true
    dc_pin: GPIO00
    reset_pin: GPIO02
    color_depth: 16
    update_interval: never
    spi_mode: mode3
    data_rate: 40000000
    auto_clear_enabled: False
    lambda: |-
      // it.filled_rectangle(0, 0, 240, 240, Color(255, 0, 0));
      // it.printf(120, 120, id(font_label), Color(255, 255, 255), TextAlign::CENTER, "HELLO");

      auto hex_to_color = [](const std::string &hex) -> Color {
        uint32_t rgb = 0x00FF88;
        if (hex.length() == 6) {
          rgb = strtoul(hex.c_str(), nullptr, 16);
        }
        uint8_t r = (rgb >> 16) & 0xFF;
        uint8_t g = (rgb >> 8) & 0xFF;
        uint8_t b = rgb & 0xFF;
        return Color(r, g, b);
      };

      int w = 240;
      int y = 0;
      int header_h = 25;
      int row_h = 34;

      // Background
      it.filled_rectangle(0, 0, w, 240, id(color_bg));

      // Header bar
      it.filled_rectangle(0, 0, w, header_h, id(color_header_bg));
      it.printf(w / 2, header_h / 2, id(font_header), id(color_header_text), TextAlign::CENTER, "AS20");

      y = header_h;

      int count = id(item_count);
      if (count > 6) count = 6;

      if (count == 0) {
        it.printf(w / 2, y + 20, id(font_label), id(color_label_text), TextAlign::CENTER, "Waiting for data...");
      }

      for (int i = 0; i < count; i++) {
        int row_y = y + i * row_h;

        // Row background
        it.filled_rectangle(0, row_y, w, row_h, id(color_row_bg));

        // Bottom border
        it.horizontal_line(0, row_y + row_h - 1, w, id(color_row_border));

        // Label on left
        it.printf(6, row_y + row_h / 2, id(font_label), id(color_label_text), TextAlign::CENTER_LEFT, "%s", id(item_labels)[i].c_str());

        // Value on right with dynamic color
        Color val_color = hex_to_color(id(item_colors)[i]);
        it.printf(w - 5, row_y + row_h / 2, id(font_value), val_color, TextAlign::CENTER_RIGHT, "%s", id(item_values)[i].c_str());
      }

As I understand, the base version (with ESP8266) doesn’t come with a button, only the screen, but the pro version (with ESP32) has a button (or maybe some capacitive sensor, whatever). Is this correct?

Also, if someone knows the answer to these, would be awesome:

  1. Does the base version have free pins so I could add in my own button to it?
    What about the pro version?

  2. Can the pro version’s button be used via ESPHome/Home Assistant just fine?

Would anyone know how to modify this so that the sunset would be displayed in 12 hour instead of 24 hour time? Or can you point me to a better place to ask?

# Text sensor for time
time:
  - platform: sntp
    id: sntp_time
    servers:
      - 0.pool.ntp.org
      - 1.pool.ntp.org   

sun:
  latitude: !secret home_latitude
  longitude: !secret home_longitude
  id: sun_position

text_sensor:
  - platform: sun
    name: "Sunrise"
    type: sunrise
    id: sun_sunrise
  - platform: sun
    name: "Sunset"
    type: sunset
    id: sun_sunset




display:  
  - platform: mipi_spi
    model: st7789v
    spi_id: spihwd
    dimensions: 
      height: 240
      width: 240
      offset_height: 0
      offset_width: 0
    invert_colors: true    
    dc_pin: GPIO00
    reset_pin: GPIO02
    #backlight_pin: GPIO25
    color_depth: 8
    #update_interval: never
    update_interval: 10s
    id: disp
    spi_mode: mode3
    lambda: |-
      // Display time and temperature
      const auto RED = Color(255, 0, 0);
      const auto GREEN = Color(0, 255, 0);
      const auto BLUE = Color(0, 0, 255);
      const auto WHITE = Color(255, 255, 255);


      // Draw time (centered)
      it.strftime(120, 20, id(font1), WHITE, TextAlign::CENTER, "%H:%M", id(sntp_time).now());
      it.strftime(120, 60, id(font1), GREEN, TextAlign::CENTER, "%l:%M %p", id(sntp_time).now());
      it.strftime(120, 100, id(font2), RED, TextAlign::CENTER, "%a", id(sntp_time).now());      
      it.strftime(120, 130, id(font2), RED, TextAlign::CENTER, "%Y/%m/%d", id(sntp_time).now());
      it.printf(0, 160, id(font3), BLUE, TextAlign::LEFT, "Next Rise: %s", id(sun_sunrise).state.c_str());
      it.printf(0, 185, id(font3), BLUE, TextAlign::LEFT, "Next Set: %s", id(sun_sunset).state.c_str());


# Fonts (add to your ESPHome project)
font:
  - file: "gfonts://Roboto"
    id: font1
    size: 48
  - file: "gfonts://Roboto"
    id: font2
    size: 33
  - file: "gfonts://Roboto"
    id: font3
    size: 25

Yeah, I use it to turn pages (tap) and perform actions (hold).

esp32_touch:
#  setup_mode: true

binary_sensor:
  - platform: esp32_touch
    name: "Touch Pad"
    disabled_by_default: True
    internal: True
    pin: GPIO32
    threshold: 1500
    id: my_touch_pad
    on_state:
      then:
        - component.update: lcd_display
    on_release:
      then:
        - script.execute: "config_page_timeout"
    on_click:
      - min_length: 10ms
        max_length: 500ms
        then:
          - if:
              condition:
                - lambda: 'return id(back_light).current_values.get_brightness() < .5;'
              then:
                - script.execute: adjust_brightness_with_timeout
          - if:
              condition:
                - lambda: 'return id(back_light).current_values.get_brightness() >= .5;'
              then:
                - logger.log: "Displaying next page..."
                - display.page.show_next: lcd_display
                - component.update: lcd_display
    on_press:
      then:
        - delay: 1s
        - if:
            condition:
              - for:
                  condition:
                    - binary_sensor.is_on: my_touch_pad
                  time: 1s
            then:
              - if:
                  condition:
                    - display.is_displaying_page: page_curtain
                  then:
                    - logger.log: "Toggling curtains..."
                    - homeassistant.action:
                        action: cover.toggle
                        data:
                          entity_id: cover.curtains

Hey! Just came here to say thanks for this topic. You folks are GOAT!
I had one of these on my junk drawer for ages. Now i have a screen that shows some network stuff.
I even wrote an article about it. It contains my working setup and some more data related to this.
Let me know what you think: I just wanted a desk clock I accidentally built a Home Assistant dashboard - DEV Community

Thanks for that wright up!
It is a very nice summary with many details I was unaware of. A very thorough and welcome addition to the knowledge being gained here

I do not know if anyone would be interested in this but I think the GeekMagic display is cool a bit big, looking like an old school TV set. I’ve been looking for a 3D printable case for it but have not found one. So I decided to create my own. It’s as basic as it get. I can upload the file if someone wants it. What’s the best place to upload in your opinion?

Printables.com, thingiverse.com or similar. These sites are specialized for printed things.

I have the same model. I have succesfully integrated it to ESPhome, but I cannot get the display working. What is your display config in the yaml?

I’m new to this (I’ve only experimented with a Lilygo S3) and in my enthusiasm, I ordered the Pro instead of the S3 version. So to be able to flash it with a basic ESPHome firmware, I need to connect GND with GPIO0 and then connect the device with the USB-cable to my pc? (Sorry, I’m afraid of damaging my pc :slight_smile: ) Do I need an USB-UART adapter for this or is there any other way to do this?

If you are new, I’d recommend starting with a simple modern ESPHome config like mine here :slight_smile:, and then adding some LVGL.

You can solder headers to the GPIOs if you want to enable modern partition features like boot loader rollback, but if you are new, try just uploading the firmware to the stock Web UI in Legacy format (which you can get in ESPHome Device Builder! That worked for me when I got mine late last year!

Ah thanks, I will take a look this week!

I got the display up and running after a few false starts with my YAML. Works great now!

Hello !
To anyone having an (apprently cheaper) GeekMagic Smart Weather clock with ESP8266 (and no touch sensitive button), using a FTDI adapter I was able to have it work fine using this code :

# ============================================================
# ESPHome base configuration for GeekMagic SmallTV
# (ESP-12F / ESP8266 + ST7789V 240x240 display, no CS pin)
#
# Tested with ESPHome 2026.5.0
#
# Hardware specifications:
#   - MCU      : ESP-12F (ESP8266MOD)
#   - Display  : ST7789V 240x240, SPI, no CS pin (tied to GND)
#   - GPIO14   : SPI CLK
#   - GPIO13   : SPI MOSI
#   - GPIO00   : DC (Data/Command)
#   - GPIO02   : RST (Display Reset)
#   - GPIO05   : Backlight (PWM, inverted)
#
# Key settings required to work:
#   - spi_mode: mode3     (mandatory without CS pin)
#   - color_depth: 8bit   (mandatory on ESP8266, limited RAM)
#   - buffer_size: 12%    (minimum accepted, prevents RAM crashes)
#   - invert_colors: true (correct colors on this model)
# ============================================================

esphome:
  name: geekmagic-smalltv

esp8266:
  board: esp12e

wifi:
  min_auth_mode: WPA2
  networks:
    - ssid: !secret wifi_ssid
      password: !secret wifi_password
  ap:
    ssid: "GeekMagic-Fallback"
    password: "fallback123"

logger:

api:
  encryption:
    key: !secret api_key

ota:
  - platform: esphome
    password: !secret ota_password

captive_portal:

# --- Backlight ---
spi:
  clk_pin: GPIO14
  mosi_pin: GPIO13

output:
  - platform: esp8266_pwm
    pin: GPIO05
    frequency: 1000 Hz
    id: pwm_backlight
    inverted: true

light:
  - platform: monochromatic
    output: pwm_backlight
    name: "Backlight"
    id: retroeclairage
    restore_mode: ALWAYS_ON
    default_transition_length: 1s

# --- Display ---
display:
  - platform: mipi_spi
    model: ST7789V
    dc_pin: GPIO00
    reset_pin: GPIO02
    dimensions:
      width: 240
      height: 240
    color_depth: 8bit
    spi_mode: mode3
    invert_colors: true
    buffer_size: 12%
    update_interval: never
    id: disp
    lambda: |-
      it.fill(Color(0, 0, 0));  // Black background

On top of that, I was able to have it display a clock, and the status of 2 entities.
I also set up a timeout to turn backlight off 90 seconds after the last movement (to prevent pixel burn-in)

About your comment on turning off backlight to prevent burn in, turning off the backlight doesn't actually help (meaningfully). The backlight just makes the image visible. The image is still displayed the whole time, whether the backlight is on or off. Read this from the ESPHome LVGL cookbook on LCD burn-in prevention:

But sure, I guess turning off the backlight also helps for the lifetime of the backlight itself, but I have no idea if that's something one should be worried about anyway.
I also think your 90 seconds timeout sounds extremely conservative, but if it doesn't bother you and you're aiming for an extremely long lifetime of the LCD, fair enough.