ESP32-C3 with integrated GC9A01 - cheap touch controller

I love it - my method is to put a yellow sticky note on the switches with “magic enabled” written on it. Works, but your solution is far more elegant. :slight_smile:

Fully understand the whole pushing the thing out before calling it a day - I did the same thing when initially putting the original post together ie did a big burst of trying to nut out the screen and then when I finally got it to work I had to put it aside for a while. But huge thank you for getting the touch code working - I agree it does pretty much everything we need. It would be nice to have the ability to identify where on the screen it’s being touched, but it’s so damn small the only use case might be for dimming the lights , changing music volume, or temperature settings - but all of those can be done with swipes as well. Anyhoo, found a few more spare minutes and have added light on/off rather than toggle.

# V0.3 - 10/10/2023
# Compiled and tested on esphome 2023.9.3
substitutions:
  devicename: wallwatch01
  friendname: WallWatch01
  location: master
  board: esp32-c3-devkitm-1
#GPIO pins for the LCD screen
  repin: GPIO1
  dcpin: GPIO2
  bkpin: GPIO3
  clpin: GPIO6
  mopin: GPIO7
  cspin: GPIO10
# GPIO pins for the touch screen
  sdapin: GPIO4
  sclpin: GPIO5
  intpin: GPIO8  

esphome:
  name: $devicename
  friendly_name: $friendname

esp32:
  board: $board
  framework:
    type: arduino

# Enable logging
# Change to avoid "Components should block for at most 20-30ms" warning messages in the log - an issue since 2023.7.0
# Not really a breaking change - it's an issue I suspect due to the device being slow and this error previously
# simply not being reported
logger:
  level: DEBUG
  logs:
    component: ERROR

# Enable Home Assistant API
api:
  encryption:
    key: !secret esphome_encryption_key

ota:
  password: !secret ota_password

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Wallwatch01 Fallback Hotspot"
    password: !secret fallback_password

captive_portal:

time:
  - platform: homeassistant
    timezone: "Australia/Melbourne"
    id: esptime

# Test - see if the thing picks up BLE info
#esp32_ble_tracker:
#nope - locks up
#SpokeTooSoon - actually does work, kinda. Slows down the startup significantly then claims to scan but nothing is found
#Either is not supported or antenna is really weak. Suspect both.

sensor:
  - platform: uptime
    name: "$devicename Uptime"
  - platform: wifi_signal
    name: "$devicename WiFi Signal"
    update_interval: 60s    
  - platform: homeassistant
    id: outdoor_temperature
    entity_id: sensor.gw1000_v1_7_6_outdoor_temperature
  - platform: homeassistant
    id: max_temperature
    entity_id: sensor.brighton_east_temp_max_0
  - platform: homeassistant
    id: min_temperature
    entity_id: sensor.brighton_east_temp_min_1
# Add in the thermostat and temp sensor we want to see
  - platform: homeassistant
    id: thermostat_temperature
    entity_id: climate.thermostat_master
    attribute: temperature
  - platform: homeassistant
    id: room_temperature
    entity_id: sensor.wall_control_06_wall06_temp

external_components:
  - source: github://zagnuts/esphome-components
    components: [ gc9a01 ]
    refresh: 30min
  - source: github://GadgetFactory/[email protected] 

spi:
  mosi_pin: $mopin
  clk_pin: $clpin
# Don't use software - makes it crawl  
#  force_sw: True
#mosi = Master Out Slave In
#miso = Master In Slave Out or fermented bean paste. In this case, most likely the former rather than the latter. Doesn't matter as not used.

i2c:
  sda: $sdapin
  scl: $sclpin
  #Following helped someone get rid of error messages but didn't for me
  #frequency: 400kHz

number:
  # Create some virtual sensors - use automations to perform actions based on the state of these
  # This is to adjust the virtual thermostat
  - platform: template
    optimistic: true
    name: "$devicename thermostat"
    id: thermostat_adjust
    min_value: 18
    max_value: 35
    initial_value: 20
    update_interval: 1s
    step: 1
  # This is a binary off or on to toggle the virtual thermostat states between off or heat or cool
  # Where 0 is off and 1 is heat and 2 is cool
  - platform: template
    optimistic: true
    name: "$devicename therm state"
    id: thermostat_state
    min_value: 0
    max_value: 2
    initial_value: 0
    update_interval: 1s
    step: 1
  # This is a binary off or on to toggle the light off or on
  - platform: template
    optimistic: true
    name: "$devicename light state"
    id: light_state
    min_value: 0
    max_value: 1
    initial_value: 0
    update_interval: 1s
    step: 1

text_sensor:
  - platform: CST816S_touchscreen
    id: my_touch_screen
    on_value:
      then:
        - if:
            condition:
              text_sensor.state:
                id: my_touch_screen
                state: 'SWIPE RIGHT'
            then:
              - display.page.show_next: watchface
              - component.update: watchface
              - logger.log: "Swiped right, go to next page"
        - if:
            condition:
              text_sensor.state:
                id: my_touch_screen
                state: 'SWIPE LEFT'
            then:
              - display.page.show_previous: watchface   
              - component.update: watchface
              - logger.log: "Swiped left, go back a page"
        - if:
            condition:
              and:
                - text_sensor.state:
                    id: my_touch_screen
                    state: 'SINGLE CLICK'
                - display.is_displaying_page: page1     
            then:
              - logger.log: "Clicked on page1"
        - if:
            condition:
              and:
                - text_sensor.state:
                    id: my_touch_screen
                    state: 'SINGLE CLICK'
                - display.is_displaying_page: page2     
            then:
              - logger.log: "Clicked on page2"
        - if:
            condition:
              and:
                - text_sensor.state:
                    id: my_touch_screen
                    state: 'SWIPE UP'
                - display.is_displaying_page: page1     
            then:
              - logger.log: "Swiped up on page1"
              #Note that this is the reverse of what you might expect
              - number.decrement: thermostat_adjust
        - if:
            condition:
              and:
                - text_sensor.state:
                    id: my_touch_screen
                    state: 'SWIPE DOWN'
                - display.is_displaying_page: page1     
            then:
              - logger.log: "Swiped down on page1"
              #Note that this is the reverse of what you might expect              
              - number.increment: thermostat_adjust
        - if:
            condition:
              and:
                - text_sensor.state:
                    id: my_touch_screen
                    state: 'SINGLE CLICK'
                - display.is_displaying_page: page4     
            then:
              - logger.log: "Single click on page4"           
              - number.increment: light_state
              # refresh the screen
              - component.update: watchface
 # Have found that the 'single click' does not always register hence also using 'long press' to toggle light 
        - if:
            condition:
              and:
                - text_sensor.state:
                    id: my_touch_screen
                    state: 'LONG PRESS'
                - display.is_displaying_page: page4     
            then:
              - logger.log: "Long press on page4"           
              - number.increment: light_state
              # refresh the screen
              - component.update: watchface

# States currently detected
# 'SWIPE UP' - note that this appears to be reversed
# 'SWIPE DOWN - note that this appears to be reversed
# 'SWIPE LEFT'
# 'SWIPE RIGHT'
# 'LONG PRESS'
# 'SINGLE CLICK'

# Need to turn on backlight as by default is not on
output:
  - platform: ledc
    pin: $bkpin
    id: gpio_3_backlight_pwm
light:
  - platform: monochromatic
    output: gpio_3_backlight_pwm
    name: "Display Backlight"
    id: back_light
    restore_mode: ALWAYS_ON

# The following assumes you have the named fonts in config\fonts
# If not, you can always download on the fly eg
#  - file: "gfonts://Roboto"
#    id: font_16
#    size: 16
font:
  - file: 'fonts/GoogleSans-Medium.ttf'
    id: font_16
    size: 16
  - file: 'fonts/GoogleSans-Medium.ttf'
    id: font_24
    size: 24
  - file: 'fonts/GoogleSans-Medium.ttf'
    id: font_32
    size: 32

color:
  - id: my_red
    red: 100%
    green: 3%
    blue: 5%
  - id: my_green
    red: 3%
    green: 100%
    blue: 5%
  - id: my_blue
    red: 3%
    green: 5%
    blue: 100%

# Some pretty images - not critical
# Again assumes you have them in config\icons
# Do not use large or complex images as the poor processor will struggle
animation:
  - file: "icons/2051v2.gif"
    id: clear_day
    resize: 40x40
    type: RGB565 #default is binary ie greyscale
  - file: "images/lightoff.png"
    id: light_off
  - file: "images/lighton.png"
    id: light_on
    type: RGB565
#    resize: 80x80
#    type: RGB565
# Need the following image section currently with esphome since 2023.7.0
# Otherwise you will probably get 'id' is a required option for [0] errors when trying to compile
# I will note here a pet hate with breaking changes - I know that this is community developed code
# but still, it's a total pain when updating brings in weird issues like this and I continue to be in 
# awe when the community discover work arounds that are so left field
image:
  - file: "icons/2051v2.gif"
    id: gifArt

display:
#  - platform: ili9xxx
#    model: gc9a01
# Above is for when or if this is merged into the ili9xxx platform
# Until then though, need to use an external component
  - platform: gc9a01
    id: watchface
    reset_pin: $repin
    cs_pin: $cspin
    dc_pin: $dcpin

# Rotate the screen so usb socket is pointing down
    rotation: 90

# Print the date on one line in blue
# Print the current time on the next line, but in a bigger font and in default white
# Print the outside temp on the next line but in green
# Surround the lot by a red circle with centre at 120, 120 and a radius of 115 because why not
# Procrastinating now on the touchscreen, so instead added a second page and cycling between two screens because again why not
    pages:
      - id: page1
        lambda: |-
          id(clear_day).next_frame();
          it.image(100, 30, id(clear_day), COLOR_ON, COLOR_OFF);
          it.printf(120,80, id(font_16), id(my_blue), TextAlign::CENTER, "Thermostat");
          it.printf(120,120, id(font_24), TextAlign::CENTER, "Set Temp: %.1f°", id(thermostat_temperature).state);
          it.printf(120, 170, id(font_32), id(my_green), TextAlign::CENTER, "Now: %.1f°", id(room_temperature).state);
          it.circle(120, 120, 115, id(my_red));
      - id: page2
        lambda: |-
          id(clear_day).next_frame();
          it.image(100, 30, id(clear_day), COLOR_ON, COLOR_OFF);
          it.strftime(120,80, id(font_16), id(my_blue), TextAlign::CENTER, "%A %b %d", id(esptime).now());
          it.strftime(120,120, id(font_32), TextAlign::CENTER, "%I:%M %p", id(esptime).now());
          it.printf(120, 170, id(font_32), id(my_green), TextAlign::CENTER, "Now: %.1f°", id(outdoor_temperature).state);
          it.circle(120, 120, 115, id(my_red));          
      - id: page3
        lambda: |-
          id(clear_day).next_frame();
          it.image(100, 30, id(clear_day), COLOR_ON, COLOR_OFF);
          it.strftime(120,80, id(font_16), id(my_blue), TextAlign::CENTER, "%A %b %d", id(esptime).now());
          it.printf(120,120, id(font_24), TextAlign::CENTER, "Today Min/Max:");
          it.printf(120, 170, id(font_32), id(my_green), TextAlign::CENTER, "%.1f/%.1f°", id(min_temperature).state,id(max_temperature).state);
          it.circle(120, 120, 115, id(my_blue));
      - id: page4
        lambda: |-
          it.image(80, 80, id(light_off), COLOR_ON, COLOR_OFF);
          if (id(light_state).state == 0)
            it.image(80, 80, id(light_off), COLOR_ON, COLOR_OFF);
          if (id(light_state).state == 1)
            it.image(80, 80, id(light_on), COLOR_ON, COLOR_OFF);
          it.circle(120, 120, 115, id(my_blue));          

and two automations

alias: Wallwatch - light turn on
description: ""
trigger:
  - platform: numeric_state
    entity_id: number.wallwatch01_wallwatch01_light_state
    above: 0
condition: []
action:
  - service: light.turn_on
    data: {}
    target:
      entity_id: light.hue_white_lamp_1_4
mode: single

and

alias: Wallwatch - light turn off
description: ""
trigger:
  - platform: numeric_state
    entity_id: number.wallwatch01_wallwatch01_light_state
    below: 1
condition: []
action:
  - service: light.turn_off
    data: {}
    target:
      entity_id: light.hue_white_lamp_1_4
mode: single

…or could combine into one automation eg:

alias: Wallwatch - turn light on or off
description: ""
trigger:
  - platform: state
    entity_id:
      - number.wallwatch01_wallwatch01_light_state
condition: []
action:
  - if:
      - condition: numeric_state
        entity_id: number.wallwatch01_wallwatch01_light_state
        above: 0
    then:
      - service: light.turn_on
        data: {}
        target:
          entity_id: light.hue_white_lamp_1_4
    else:
      - service: light.turn_off
        data: {}
        target:
          entity_id: light.hue_white_lamp_1_4
mode: single


2 Likes