Hoy te enseño a usar otro interesante dispositivo con pantalla, vamos a integrar el MaTouch ESP32-S3 Rotary 1.28″ en HA.
Índice
MaTouch ESP32-S3 Rotary 1,28″
Te presento otro dispositivo fabricado por Makerfabs. Se trata de una pantalla GC9A01 táctil circular de 1,28″ (240×240 pixels), RGB. Además del control táctil, nos permite navegar con ella con el encoder que trae alrededor y un botón al pulsar la pantalla. Por sus dimensiones reducidas y su placa perforada me parece perfecto para integrarlo en otros dispositivos o muebles.
Además funciona sobre una placa ESP32-S3, por lo que podemos integrarla con ESPHome y hacer muchas más cosas. Tiene Wi-Fi y Bluetooth 5.0 por lo que, entre otras, podemos usarla como Bluetooth Proxy con Bermuda. Se alimenta con un cable USB-C, tiene lector de tarjeta Micro SD y dos conectores de expansión (I2C y GPIO), por lo que puedes añadir sensores externos. También puedes conectar una batería Lipo de 3.7V.


Requisitos previos
Para integrar el MaTouch ESP32-S3 Rotary 1.28″ en HA previamente necesitas:
- Una pantalla MaTouch ESP32-S3 Rotary 1,28″.
- Haber instalado ESPHome en Home Assistant.
- Un cable USB-C para alimentar la placa de DATOS (con un cable de carga no vas a poder instalar el software).
- Opcionalmente, el soporte que hemos diseñado.
Configuración en ESPHome
Sigue estos pasos para integrar el MaTouch ESP32-S3 Rotary 1.28″ en HA
- En Home Assistant, ve a tu complemento de ESPHome, pulsa en “New device” y “Continue”.
- Dale un nombre a tu dispositivo (por ejemplo, “MaTouch Rotary GC9A01”) y pulsa en “Next”.
- Como tipo de dispositivo selecciona “ESP32-S3”. Observarás de fondo que se ha creado un nuevo bloque para tu dispositivo.
- Pulsa en “Skip” y haz clic en “Edit”, sobre el bloque de tu dispositivo. Copia el código que aparece y consérvalo, ya que vas a necesitar alguna parte del mismo.
- Copia el siguiente código y úsalo para reemplazar el código anterior en ESPHome.
substitutions:
name: matouch-rotary-gc9a01
friendly_name: Matouch Rotary GC9A01
esphome:
name: "${name}"
friendly_name: "${friendly_name}"
min_version: 2024.11.0
name_add_mac_suffix: false
platformio_options:
board_build.flash_mode: dio
board_build.arduino.memory_type: qio_opi
build_flags: "-DBOARD_HAS_PSRAM"
board_upload.maximum_ram_size: 524288
esp32:
board: esp32-s3-devkitc-1
flash_size: 16MB
framework:
type: esp-idf
version: recommended
sdkconfig_options:
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: y
CONFIG_ESP32S3_DATA_CACHE_64KB: y
CONFIG_SPIRAM_FETCH_INSTRUCTIONS: y
CONFIG_SPIRAM_RODATA: y
psram:
speed: 80MHz
mode: octal
# Enable logging
logger:
level: DEBUG
debug:
update_interval: 30s
# Enable Home Assistant API
api:
encryption:
key: "Wvasddsa2323Ao78hdsKwLvqUTVdsffds3432"
ota:
- platform: esphome
password: "sdf23dsb300741930aa36a9dsf32"
mdns:
disabled: false
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Matouch-Rotary-Gc9A01"
password: "lQsafpM678"
captive_portal:
binary_sensor:
- platform: gpio
name: Button
id: front_button
pin:
number: GPIO6
inverted: True
i2c:
- id: i2c_bus
sda: 17
scl: 18
scan: true
frequency: 400kHz
- id: i2c_touch
sda: 38
scl: 39
scan: true
frequency: 400kHz
light:
- platform: monochromatic
name: "Backlight"
output: backlight_output
id: display_backlight
restore_mode: ALWAYS_ON
output:
- platform: ledc
pin: GPIO7
id: backlight_output
sensor:
- platform: rotary_encoder
id: encoder
pin_a: GPIO9
pin_b: GPIO10
spi:
id: spi_bus
mosi_pin: GPIO13
clk_pin: GPIO14
time:
- platform: homeassistant
id: esptime
touchscreen:
platform: cst816
id: touch
interrupt_pin: GPIO40
reset_pin: GPIO16
i2c_id: i2c_touch
on_touch:
- logger.log:
format: Touch at (%d, %d)
args: [touch.x, touch.y]
- lambda: |-
ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%0d",
touch.x,
touch.y,
touch.x_raw,
touch.y_raw
);
display:
- platform: ili9xxx
id: round_display
model: GC9A01A
cs_pin: GPIO15
reset_pin: GPIO11
dc_pin: GPIO21
invert_colors: true
auto_clear_enabled: false
color_order: bgr
rotation: 90
lvgl:
displays:
- round_display
touchscreens:
- touch
encoders:
sensor: encoder
enter_button: front_button
buffer_size: 100%
widgets:
- label:
align: CENTER
id: lbl_title
text_font: montserrat_28
text: "Aguacatec"
- Parte importante. En este código no se han incluido las credenciales para que el dispositivo se conecte a tu WiFi y tu instancia de Home Assistant y tienes que incluirlas manualmente. En concreto me refiero a las siguientes líneas del código que has copiado en el paso 4.
# Enable Home Assistant API
api:
encryption:
key: "bg6hash6sjdjsdjk02hh0qnQeYVwm123vdfKE8BP5"
ota:
- platform: esphome
password: "asddasda27aab65a48484502b332f"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Assist Fallback Hotspot"
password: "ZsasdasdHGP2234"
- Lo que tienes que hacer es, encontrar las líneas correspondientes en el código (está al principio) y añadir la información correspondiente.
- Ahora sí, pulsa en “Save” e “Install”. Selecciona “Manual download” y espera a que se compile el código.
- Cuando termine, selecciona la opción “Modern format” para descargar el fichero ‘.bin’ correspondiente.
- Conecta el Matouch Rotary con el cable USB-C de datos por el puerto que trae en la parte posterior a tu ordenador.
- Ahora ve a la página de ESPHome y pulsa en “Connect”. En la ventana emergente selecciona tu placa y pulsa en “Conectar”.
- Ahora pulsa en “Install” y selecciona el fichero ‘.bin’ obtenido en el paso 9. De nuevo, pulsa en “Install”.
- Vuelve a Home Assistant y ve a Configuración > Dispositivos y servicios. Lo normal es que tu dispositivo haya sido descubierto y aparezca en la parte superior, esperando únicamente a que pulses el botón de “Configurar”. De lo contrario pulsa en el botón de “Añadir integración”, busca “ESPHome” e introduce la IP de tu placa en el campo ‘Host’. Como siempre, te recomiendo que asignes una IP fija en tu router para evitar fallos en el futuro si esta cambia.
- Para terminar ve a Configuración > Dispositivos y servicios > ESPHome. Pulsa sobre el enlace «Configurar» correspondiente tu dispositivo. En la ventana emergente marca la casilla «Permitir que el dispositivo realice acciones de Home Assistant» y pulsa en «Enviar». Esto va a permitir que podamos controlar nuestros dispositivos desde la pantalla.
Si todo ha ido bien, deberías estar viendo lo siguiente:

Código del PokeDesk
Como es habitual, cuando cae en nuestras manos un dispositivo así nos gusta hacer algo interesante. En este caso la forma y los colores de la placa me lo estaba pidiendo a gritos. He creado un controlador de escritorio inspirado en la estética del popular juego «Pokemon», para manejar tus dispositivos. Tanto si eres fan del anime como si no, creo que la estética del dispositivo es muy atractiva!
A continuación te dejo el código completo para que lo adaptes como quieras, así como el enlace al soporte que he diseñado.
substitutions:
# Device customization
# Personalización del dispositivo
name: "matouch-rotary-gc9a01"
friendly_name: "Matouch Rotary GC9A01"
# Example of Devices
# Ejemplo de Dispositivos
device1: light.tira_led_escritorio
device1_name: "Luz"
device1_brightness: sensor.light_brightness
device2: climate.salon
device2_name: "Calefacción"
device2_temperature: sensor.climate_temperature
device3: switch.ventilador
device3_name: "Ventilador"
device4: switch.impresora_3d
device4_name: "Impresora"
device5: vacuum.roborock_qr_598
device5_name: "Aspirador"
# Other settings
# Otros ajustes
allowed_characters: " ¿?¡!#%'()+,-./:°0123456789ABCDEFGHIJKLMNOPQRSTUVWYZabcdefghijklmnopqrstuvwxyzáéíóú"
################################################################################################################
esphome:
name: "${name}"
friendly_name: "${friendly_name}"
min_version: 2024.11.0
name_add_mac_suffix: false
platformio_options:
board_build.flash_mode: dio
board_build.arduino.memory_type: qio_opi
build_flags: "-DBOARD_HAS_PSRAM"
board_upload.maximum_ram_size: 524288
esp32:
board: esp32-s3-devkitc-1
flash_size: 16MB
framework:
type: esp-idf
version: recommended
sdkconfig_options:
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: y
CONFIG_ESP32S3_DATA_CACHE_64KB: y
CONFIG_SPIRAM_FETCH_INSTRUCTIONS: y
CONFIG_SPIRAM_RODATA: y
psram:
speed: 80MHz
mode: octal
# Enable logging
logger:
level: DEBUG
debug:
update_interval: 30s
# Enable Home Assistant API
api:
encryption:
key: "dsf4534sdfkAo78he1nKwLvsdfdf45"
ota:
- platform: esphome
password: "sdfsdf2300741930efwda98dsf23"
mdns:
disabled: false
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Matouch-Rotary-Gc9A01"
password: "lQE0Hasd5612"
captive_portal:
binary_sensor:
- platform: gpio
name: Button
id: front_button
pin:
number: GPIO6
inverted: True
on_press:
then:
- lvgl.page.show: page_home
- light.turn_on: display_backlight
- if:
condition:
light.is_on: display_backlight
then:
- if:
condition:
lambda: |-
return id(device_menu).state == "${device1_name}";
then:
- homeassistant.action:
service: light.toggle
data:
entity_id: ${device1}
- delay: 1s
- lvgl.label.update:
id: lbl_device_state
text: !lambda |-
if (id(device_1).state == "off") {
return "Apagada";
} else {
return (std::to_string((int)id(light_brightness).state) + "%").c_str();
}
- if:
condition:
lambda: |-
return id(device_1).state == "on";
then:
- lvgl.widget.show:
- id: arc_light
else:
- lvgl.widget.hide:
- id: arc_light
- if:
condition:
lambda: |-
return id(device_menu).state == "${device2_name}";
then:
- homeassistant.action:
service: climate.toggle
data:
entity_id: ${device2}
- delay: 1s
- lvgl.label.update:
id: lbl_device_state
text: !lambda |-
if (id(device_2).state == "off") {
return "Apagada";
} else {
return (std::to_string((int)id(climate_temperature).state) + "°C").c_str();
}
- if:
condition:
lambda: |-
return id(device_2).state == "heat";
then:
- lvgl.widget.show:
- id: arc_climate
else:
- lvgl.widget.hide:
- id: arc_climate
- if:
condition:
lambda: |-
return id(device_menu).state == "${device3_name}";
then:
- homeassistant.action:
service: switch.toggle
data:
entity_id: ${device3}
- delay: 1s
- lvgl.label.update:
id: lbl_device_state
text: !lambda |-
return std::string(id(device_3).state == "on" ? "Encendido" : "Apagado");
- if:
condition:
lambda: |-
return id(device_menu).state == "${device4_name}";
then:
- homeassistant.action:
service: switch.toggle
data:
entity_id: ${device4}
- delay: 1s
- lvgl.label.update:
id: lbl_device_state
text: !lambda |-
return std::string(id(device_4).state == "on" ? "Encendida" : "Apagada");
- if:
condition:
lambda: |-
return id(device_menu).state == "${device5_name}";
then:
- if:
condition:
lambda: 'return id(device_5).state == "cleaning";'
then:
- homeassistant.action:
service: vacuum.pause
data:
entity_id: ${device5}
else:
- homeassistant.action:
service: vacuum.start
data:
entity_id: ${device5}
- delay: 1s
- lvgl.label.update:
id: lbl_device_state
text: !lambda |-
return std::string(id(device_5).state == "charging" ? "En la base" : "Limpiando");
font:
- file:
type: gfonts
family: Roboto
weight: bold
id: font_menu
size: 18
glyphs: ${allowed_characters}
- file:
type: gfonts
family: Roboto
weight: bold
id: font_state
size: 14
glyphs: ${allowed_characters}
i2c:
- id: i2c_bus
sda: 17
scl: 18
scan: true
frequency: 400kHz
- id: i2c_touch
sda: 38
scl: 39
scan: true
frequency: 400kHz
image:
- file: https://aguacatec.es/wp-content/uploads/2025/05/pokeball_off.jpg
id: pokeball_off
resize: 240x240
type: RGB565
transparency: alpha_channel
- file: https://aguacatec.es/wp-content/uploads/2025/05/pokeball_on.jpg
id: pokeball_on
resize: 240x240
type: RGB565
transparency: alpha_channel
- file: https://aguacatec.es/wp-content/uploads/2025/05/pokemon_luz.jpg
id: pokemon_luz
resize: 240x240
type: RGB565
transparency: alpha_channel
- file: https://aguacatec.es/wp-content/uploads/2025/05/pokemon_calefaccion.jpg
id: pokemon_calefaccion
resize: 240x240
type: RGB565
transparency: alpha_channel
- file: https://aguacatec.es/wp-content/uploads/2025/05/pokemon_ventilador.jpg
id: pokemon_ventilador
resize: 240x240
type: RGB565
transparency: alpha_channel
- file: https://aguacatec.es/wp-content/uploads/2025/05/pokemon_impresora.jpg
id: pokemon_impresora
resize: 240x240
type: RGB565
transparency: alpha_channel
- file: https://aguacatec.es/wp-content/uploads/2025/05/pokemon_aspirador.jpg
id: pokemon_aspirador
resize: 240x240
type: RGB565
transparency: alpha_channel
light:
- platform: monochromatic
name: "Backlight"
output: backlight_output
id: display_backlight
restore_mode: ALWAYS_ON
on_turn_off:
lvgl.animimg.stop: animation_pokeball
on_turn_on:
select.first:
id: device_menu
output:
- platform: ledc
pin: GPIO7
id: backlight_output
select:
- platform: template
name: Dispositivos
id: device_menu
optimistic: true
options:
- ${device1_name}
- ${device2_name}
- ${device3_name}
- ${device4_name}
- ${device5_name}
on_value:
then:
- lvgl.label.update:
id: lbl_device
text: !lambda 'return x;'
- lvgl.image.update:
id: pokemon
src: !lambda |-
if (x == "${device1_name}") {
return id(pokemon_luz);
} else if (x == "${device2_name}") {
return id(pokemon_calefaccion);
} else if (x == "${device3_name}") {
return id(pokemon_ventilador);
} else if (x == "${device4_name}") {
return id(pokemon_impresora);
} else if (x == "${device5_name}") {
return id(pokemon_aspirador);
} else {
return id(pokeball_off); // Valor predeterminado
}
- lvgl.label.update:
id: lbl_device_state
text: !lambda |-
if (x == "${device1_name}") {
if ((id(device_1).state == "off") || isnan(id(light_brightness).state)) {
return "Apagada";
} else {
return (std::to_string((int)id(light_brightness).state) + "%").c_str();
}
} else if (x == "${device2_name}") {
if (id(device_2).state == "off") {
return "Apagada";
} else {
return (std::to_string((int)id(climate_temperature).state) + "°C").c_str();
}
} else if (x == "${device3_name}") {
return id(device_3).state == "on" ? "Encendido" : "Apagado";
} else if (x == "${device4_name}") {
return id(device_4).state == "on" ? "Encendida" : "Apagada";
} else if (x == "${device5_name}") {
return id(device_5).state == "docked" ? "En la base" : "Limpiando";
} else {
return "Bienvenido!";
}
- if:
condition:
lambda: |-
return id(device_menu).state == "${device1_name}" && id(device_1).state == "on";
then:
- lvgl.widget.show:
- id: arc_light
else:
- lvgl.widget.hide:
- id: arc_light
- if:
condition:
lambda: |-
return id(device_menu).state == "${device2_name}" && id(device_2).state == "heat";
then:
- lvgl.widget.show:
- id: arc_climate
else:
- lvgl.widget.hide:
- id: arc_climate
sensor:
- platform: rotary_encoder
id: encoder
pin_a: GPIO9
pin_b: GPIO10
on_clockwise:
then:
- lvgl.page.show: page_home
- light.turn_on: display_backlight
- select.next:
id: device_menu
cycle: true
on_anticlockwise:
then:
- lvgl.page.show: page_home
- light.turn_on: display_backlight
- select.previous:
id: device_menu
cycle: true
- platform: homeassistant
id: light_brightness
entity_id: ${device1_brightness}
on_value:
then:
- lvgl.arc.update:
id: arc_light
value: !lambda 'return (int)x;'
- if:
condition:
lambda: |-
return id(device_menu).state == "${device1_name}" && id(device_1).state == "on";
then:
- lvgl.label.update:
id: lbl_device_state
text:
format: "%.0f%%"
args:
- id(light_brightness).state
- platform: homeassistant
id: climate_temperature
entity_id: ${device2_temperature}
on_value:
then:
- lvgl.arc.update:
id: arc_climate
value: !lambda 'return (int)x;'
- if:
condition:
lambda: |-
return id(device_menu).state == "${device2_name}" && id(device_2).state == "heat";
then:
- lvgl.label.update:
id: lbl_device_state
text:
format: "%.0f °C"
args:
- id(climate_temperature).state
spi:
id: spi_bus
mosi_pin: GPIO13
clk_pin: GPIO14
text_sensor:
- platform: homeassistant
id: device_1
entity_id: ${device1}
internal: true
- platform: homeassistant
id: device_2
entity_id: ${device2}
internal: true
- platform: homeassistant
id: device_3
entity_id: ${device3}
internal: true
- platform: homeassistant
id: device_4
entity_id: ${device4}
internal: true
- platform: homeassistant
id: device_5
entity_id: ${device5}
internal: true
time:
- platform: homeassistant
id: esptime
touchscreen:
platform: cst816
id: touch
interrupt_pin: GPIO40
reset_pin: GPIO16
i2c_id: i2c_touch
transform:
mirror_x: false
mirror_y: true
swap_xy: true
on_touch:
- logger.log:
format: Touch at (%d, %d)
args: [touch.x, touch.y]
- lambda: |-
ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%0d",
touch.x,
touch.y,
touch.x_raw,
touch.y_raw
);
on_release:
then:
- if:
condition: lvgl.is_paused
then:
- lvgl.page.show: page_home
- light.turn_on: display_backlight
- lvgl.resume:
- lvgl.widget.redraw:
display:
- platform: ili9xxx
id: round_display
model: GC9A01A
cs_pin: GPIO15
reset_pin: GPIO11
dc_pin: GPIO21
invert_colors: true
auto_clear_enabled: false
color_order: bgr
rotation: 90
lvgl:
displays:
- round_display
touchscreens:
- touch
encoders:
sensor: encoder
enter_button: front_button
buffer_size: 25%
style_definitions:
- id: style_container
align: CENTER
bg_color: 0xffffff
bg_opa: 60%
y: 100
width: 240
height: 60
border_width: 0
pad_top: -15
- id: style_roller
text_font: font_menu
text_color: 0x000000
align: CENTER
pad_top: 0
- id: style_device_state
text_font: font_state
text_color: 0x888888
align: CENTER
pad_top: 32
- id: style_arc
align: CENTER
width: 225
height: 225
arc_color: 0xe0e0e0
on_idle:
- timeout: 5s
then:
- lvgl.animimg.start: animation_pokeball
- logger.log: idle 5s timeout
- lvgl.page.show: page_iddle
- delay: 5s
- lvgl.pause:
- light.turn_off:
id: display_backlight
transition_length: 5s
pages:
- id: page_iddle
widgets:
- animimg:
align: CENTER
id: animation_pokeball
src: [ pokeball_off, pokeball_on ]
duration: 1000ms
- id: page_home
scrollbar_mode: "OFF"
widgets:
- image:
align: CENTER
src: pokemon_luz
id: pokemon
- obj:
styles: style_container
widgets:
- label:
styles: style_roller
id: lbl_device
text: "Aguacatec"
- label:
styles: style_device_state
id: lbl_device_state
text: "Bienvenido!"
- arc:
styles: style_arc
id: arc_light
indicator:
arc_color: 0xffba00
knob:
bg_color: 0xffba00
adjustable: true
value: 50
min_value: 0
max_value: 100
hidden: true
start_angle: 150
end_angle: 30
on_release:
- homeassistant.action:
action: light.turn_on
data:
entity_id: ${device1}
brightness_pct: !lambda return int(x);
- arc:
styles: style_arc
id: arc_climate
indicator:
arc_color: 0xff6600
knob:
bg_color: 0xff6600
adjustable: true
value: 20
min_value: 5
max_value: 25
hidden: true
start_angle: 150
end_angle: 30
on_release:
- homeassistant.action:
action: climate.set_temperature
data:
entity_id: ${device2}
temperature: !lambda return int(x);

