DIY & firmware
Exemplos prontos de ESPHome
Configurações completas de ESPHome prontas para copiar — Bluetooth Proxy, receptor de IR, dispositivos Thread e telas — para o seu Home Assistant.
Configurações completas de ESPHome, prontas para copiar e adaptar ao seu Home Assistant. Cada exemplo já vem com os dados sensíveis redigidos (MACs, IPs, chaves de API e datasets Thread como exemplos) — troque pelos seus antes de gravar.
Novo no ESPHome? Comece pelo artigo Sensores com ESPHome e ESP32.
Índice#
- Bluetooth Proxy (ATOM-S3)
- Receptor de infravermelho (IR)
- Dispositivo Thread (NanoC6)
- Painel de toque (clima, cortinas e ar)
- Tela Thread com temperatura e umidade (C6-GEEK)
- Painel de parede 7" para a casa toda (Waveshare 7B)
- Assistente de voz com tela (AtomS3R + Echo Base)
- Leitor RFID/NFC com tela (AtomS3R + RFID 2)
Exemplo pronto: Bluetooth Proxy com ATOM-S3#
Um Bluetooth Proxy estende o alcance Bluetooth do Home Assistant: o ESP32 repassa, pela rede, os sinais BLE de dispositivos próximos (sensores, lâmpadas, etiquetas). Aqui, num M5Stack ATOM-S3 (ESP32-S3), ele também traz um LED que muda de cor conforme o Wi-Fi e um pequeno sniffer opcional que registra no log os anúncios BLE de lâmpadas Philips Hue — útil para diagnóstico.
Troque pelos seus dados antes de gravar. A chave de criptografia da API, a senha de OTA, o IP fixo e as chaves
!secretabaixo são apenas exemplos. Nunca publique chaves, senhas, MACs ou IPs reais — gere os seus.
# Bluetooth Proxy em M5Stack ATOM-S3 (ESP32-S3), com LED de status Wi-Fi
# e um sniffer opcional de anúncios BLE da Philips Hue (diagnóstico).
# Troque a chave da API, a senha de OTA, o IP fixo e as chaves !secret.
esphome:
name: atom-s3-bt-proxy
friendly_name: "ATOM-S3 BT PROXY"
on_boot:
priority: -10
then:
- script.execute: atualizar_led_wifi
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
framework:
type: esp-idf
flash_size: 8MB
logger:
level: DEBUG
api:
encryption:
key: !secret api_encryption_key # gere a sua no ESPHome Builder
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
manual_ip: # ajuste para a sua rede (ou remova para usar DHCP)
static_ip: 192.168.1.51
gateway: 192.168.1.1
subnet: 255.255.255.0
power_save_mode: NONE
on_connect:
then:
- script.execute: atualizar_led_wifi
on_disconnect:
then:
- light.turn_on:
id: led_rgb
red: 100%
green: 0%
blue: 0%
brightness: 100%
ap:
ssid: atom-s3-bt-proxy
captive_portal:
light:
- platform: esp32_rmt_led_strip
id: led_rgb
name: "LED RGB Interno"
pin: GPIO35
num_leds: 1
chipset: WS2812
channel_colors: GRB
restore_mode: ALWAYS_OFF
esp32_ble_tracker:
scan_parameters:
interval: 1100ms
window: 1100ms
active: true
# Filtro opcional em C++ (lambda): registra no log apenas anúncios da Philips Hue
on_ble_advertise:
then:
- lambda: |-
// 1. Filtrar pelo UUID de serviço da Signify (0xFE0F), se disponível
bool is_hue = false;
for (auto data : x.get_service_uuids()) {
if (data.to_string() == "0000fe0f-0000-1000-8000-00805f9b34fb" || data.to_string() == "0XFE0F") {
is_hue = true;
}
}
// 2. Filtrar pelos dados de fabricante (Manufacturer Data ID 0xFE0F)
for (auto data : x.get_manufacturer_datas()) {
if (data.uuid.to_string() == "0XFE0F" || data.uuid.to_string() == "0000fe0f-0000-1000-8000-00805f9b34fb") {
is_hue = true;
}
}
// Se for uma lâmpada Hue, imprime as informações brutas no log
if (is_hue) {
ESP_LOGI("HUE_SNIFFER", "Lampada Hue encontrada!");
ESP_LOGI("HUE_SNIFFER", " Endereco MAC: %s", x.address_str().c_str());
ESP_LOGI("HUE_SNIFFER", " Nome BLE: %s", x.get_name().c_str());
ESP_LOGI("HUE_SNIFFER", " Sinal (RSSI): %d dBm", x.get_rssi());
for (auto data : x.get_manufacturer_datas()) {
std::string hex_data = "";
for (uint8_t byte : data.data) {
char buf[4];
sprintf(buf, "%02X ", byte);
hex_data += buf;
}
ESP_LOGI("HUE_SNIFFER", " Manufacturer Data [%s]: %s", data.uuid.to_string().c_str(), hex_data.c_str());
}
}
bluetooth_proxy:
active: true
script:
- id: atualizar_led_wifi
mode: restart
then:
- if:
condition:
wifi.connected:
then:
- light.turn_on:
id: led_rgb
red: 0%
green: 100%
blue: 0%
brightness: 100%
else:
- light.turn_on:
id: led_rgb
red: 100%
green: 0%
blue: 0%
brightness: 100%
button:
- platform: restart
name: "Reiniciar"
- platform: safe_mode
name: "Safe Mode"
binary_sensor:
- platform: status
name: "Status ESPHome"
entity_category: diagnostic
sensor:
- platform: wifi_signal
name: "Sinal Wi-Fi"
update_interval: 60s
entity_category: diagnostic
- platform: uptime
name: "Tempo Ligado"
update_interval: 60s
entity_category: diagnostic
text_sensor:
- platform: wifi_info
ip_address:
name: "WI-FI IP Address"
entity_category: diagnostic
dns_address:
name: "DNS Address"
entity_category: diagnostic
mac_address:
name: "WI-FI MAC Address"
entity_category: diagnostic
ssid:
name: "SSID"
entity_category: diagnostic
bssid:
name: "BSSID"
entity_category: diagnostic
safe_mode:
Como funciona, em resumo:
bluetooth_proxy+esp32_ble_trackerfazem o ESP32 repassar ao Home Assistant os sinais BLE que ele enxerga — ampliando a cobertura Bluetooth da casa.- O LED fica verde quando o Wi-Fi conecta e vermelho quando cai, controlado pelo
script. - O bloco
lambdaé um filtro opcional: identifica anúncios da Philips Hue (UUID de serviço0xFE0F, da Signify) e imprime no log o MAC, o RSSI e os bytes do fabricante — apenas para diagnóstico. - Sensores de diagnóstico (sinal Wi-Fi, uptime, IP, MAC, SSID) ajudam a monitorar o dispositivo; os botões Reiniciar e Safe Mode facilitam a recuperação.
Valide no ESPHome Device Builder (Validate) e teste um componente por vez.
Exemplo pronto: receptor de infravermelho (IR) no Home Assistant#
Quer que o Home Assistant "aprenda" os códigos do controle remoto da TV, do ar-condicionado ou do som? Um receptor de infravermelho ligado a um ESP32 captura esses sinais. Com remote_receiver e dump: all, cada botão pressionado aparece no log do ESPHome com o protocolo e o código — pronto para reutilizar numa automação ou num controle virtual.
Troque pelos seus dados antes de gravar. A chave de criptografia da API, o IP fixo, o DNS e as chaves
!secretabaixo são apenas exemplos. Nunca publique chaves, senhas ou IPs reais — gere os seus.
# Receptor de infravermelho (IR) em ESP32, exposto ao Home Assistant.
# Troque a chave da API, o IP fixo, o DNS e as chaves !secret pelos seus.
esphome:
name: esp32-ir-receiver-escritorio
friendly_name: "ESP32-IR-RECEIVER-ESCRITORIO"
esp32:
board: esp32-devkit-v1
variant: esp32
framework:
type: esp-idf
logger:
level: DEBUG
api:
encryption:
key: !secret api_encryption_key # gere a sua no ESPHome Builder
ota:
- platform: esphome
encryption:
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: NONE
manual_ip: # ajuste para a sua rede (ou remova para usar DHCP)
static_ip: 192.168.1.52
gateway: 192.168.1.1
subnet: 255.255.255.0
dns1: 1.1.1.1
dns2: 8.8.8.8
remote_receiver:
id: ir_rx
pin:
number: GPIO27
inverted: true
mode:
input: true
pullup: true
dump: all # registra no log os códigos de todos os protocolos
tolerance: 25%
# Expõe o receptor à integração de infravermelho do Home Assistant.
infrared:
- platform: ir_rf_proxy
id: receptor_lg_escritorio
name: "Receptor IR LG ESCRITORIO"
receiver_frequency: 38kHz
remote_receiver_id: ir_rx
icon: "mdi:remote"
button:
- platform: restart
name: "Reiniciar"
- platform: safe_mode
name: "Safe Mode"
binary_sensor:
- platform: status
name: "Status ESPHome"
entity_category: diagnostic
text_sensor:
- platform: wifi_info
ip_address:
name: "WI-FI IP Address"
entity_category: diagnostic
dns_address:
name: "DNS Address"
entity_category: diagnostic
mac_address:
name: "WI-FI MAC Address"
entity_category: diagnostic
Como funciona, em resumo:
- Um sensor de IR (por exemplo VS1838B/TSOP, 38 kHz) é ligado ao pino
GPIO27, compullup. remote_receivercomdump: alldecodifica e registra no log os sinais de vários protocolos — é assim que você descobre o código de cada botão.- O bloco
infraredexpõe o receptor à integração de infravermelho do Home Assistant. - Sensores de diagnóstico (IP, DNS, MAC) e os botões Reiniciar e Safe Mode ajudam na manutenção.
Confira a pinagem do seu receptor (VCC, GND e sinal) e valide no ESPHome Device Builder antes de gravar.
Exemplo pronto: dispositivo Thread com M5Stack NanoC6#
Thread é uma rede sem fio de malha (mesh), de baixo consumo, que serve de base para o Matter. O M5Stack NanoC6 (ESP32-C6) tem o rádio 802.15.4 necessário e, com o componente openthread, entra na sua rede Thread como um dispositivo FTD (roteador). O LED RGB sinaliza o estado: pisca vermelho sem conexão com o Home Assistant e fica azul quando conecta.
Dado MUITO sensível. O campo
tlv(o dataset) do Thread contém a chave da sua rede — quem tiver esse valor entra na sua malha. Nunca o publique. A chave da API e os demais dados abaixo também são exemplos; gere os seus.
# M5Stack NanoC6 (ESP32-C6) como dispositivo Thread (OpenThread).
# Troque a chave da API e, principalmente, o dataset TLV da sua rede Thread.
esphome:
name: nanoc6-thread
friendly_name: "NanoC6 Thread"
on_boot:
# Prioridade -10: executa só depois que luzes e switches já existem.
priority: -10
then:
- switch.turn_on: pwr_g19 # garante a energia do LED RGB
- delay: 1s
- light.turn_on:
id: rgb_led
effect: "NanoC6 Thread Blink_Red"
esp32:
board: esp32-c6-devkitc-1
framework:
type: esp-idf
version: recommended
sdkconfig_options:
CONFIG_ESP_TASK_WDT_TIMEOUT_S: "60" # watchdog maior p/ FTD (init pode demorar)
CONFIG_PM_ENABLE: "n"
CONFIG_ESP_INT_WDT_TIMEOUT_MS: "2000"
logger:
level: DEBUG
api:
encryption:
key: !secret api_encryption_key # gere a sua no ESPHome Builder
on_client_connected:
- light.turn_on:
id: rgb_led
red: 0.0
green: 0.0
blue: 1.0
brightness: 1.0
effect: none
on_client_disconnected:
- light.turn_on:
id: rgb_led
effect: "NanoC6 Thread Blink_Red"
ota:
- platform: esphome
encryption:
# IPv6 é necessário para o Thread
network:
enable_ipv6: true
# Rádio Thread. ATENÇÃO: o 'tlv' (dataset) contém a CHAVE DA SUA REDE THREAD.
# Nunca publique o seu; cole o dataset da sua rede aqui.
openthread:
device_type: FTD
tlv: "COLE_AQUI_O_DATASET_TLV_DA_SUA_REDE_THREAD"
force_dataset: true
output_power: 20dBm
# ===== Periféricos do M5 Nano-C6 =====
# LED de diagnóstico nativo do ESPHome no pino 7
status_led:
pin: GPIO7
switch:
- platform: gpio
pin: GPIO19
id: pwr_g19
name: "NanoC6 Thread Energia Periféricos"
restore_mode: ALWAYS_ON
- platform: restart
id: restart_switch
name: "NanoC6 Thread Restart Switch"
# LED RGB (GPIO20)
light:
- platform: esp32_rmt_led_strip
id: rgb_led
name: "Nano C6 RGB"
pin: GPIO20
num_leds: 1
chipset: WS2812
channel_colors: GRB
effects:
- strobe:
name: "NanoC6 Thread Blink_Red"
colors:
- state: true
red: 1.0
green: 0.0
blue: 0.0
brightness: 1.0
duration: 500ms
- state: false
duration: 500ms
button:
- platform: restart
id: restart_device
name: "Reiniciar Nano C6"
# Botão físico (GPIO9): segurar por 5s reinicia
binary_sensor:
- platform: gpio
id: btn_gpio9
name: "Botão M5Stack"
pin:
number: GPIO9
inverted: true
mode:
input: true
pullup: true
filters:
- delayed_on: 5s
on_press:
- button.press: restart_device
- platform: status
name: "NanoC6 Thread Status de Conexão"
id: status_conexao
# ===== Sensores de diagnóstico =====
sensor:
- platform: uptime
name: "NanoC6 Thread Tempo de Atividade"
id: uptime_sensor
update_interval: 60s
filters:
- lambda: return x / 60.0;
unit_of_measurement: "min"
icon: mdi:clock-outline
text_sensor:
- platform: openthread_info
ip_address:
name: "Thread IP Address"
channel:
name: "Thread Channel"
role:
name: "Thread Device Role"
rloc16:
name: "Thread RLOC16"
ext_addr:
name: "Thread Extended Address"
eui64:
name: "Thread EUI64"
network_name:
name: "Thread Network Name"
network_key:
name: "Thread Network Key"
pan_id:
name: "Thread PAN ID"
ext_pan_id:
name: "Thread Extended PAN ID"
Como funciona, em resumo:
openthreadcomdevice_type: FTDfaz o NanoC6 participar da rede Thread; otlvé o dataset que descreve a rede (canal, PAN ID e a chave) — por isso é sigiloso. Onetwork: enable_ipv6é obrigatório.- O LED RGB dá feedback visual: azul com o HA conectado, vermelho piscando sem conexão.
- O
switchemGPIO19liga a energia dos periféricos do NanoC6 (incluindo o LED); o botão físico emGPIO9, segurado por 5 s, reinicia. - Os
text_sensordoopenthread_infoexpõem o diagnóstico da rede (IP, canal, papel, PAN ID etc.) no Home Assistant.
Obtenha o dataset TLV da sua rede Thread (pelo Home Assistant ou pelo seu Border Router) e valide no ESPHome Device Builder antes de gravar.
Exemplo pronto: painel de toque (clima, cortinas e ar)#
Uma telinha de toque na parede que mostra relógio, data e clima e controla cortinas, ventilador e ar-condicionado direto no Home Assistant. Este exemplo usa uma placa Guition JC3248W535 (ESP32-S3, 3,5" 320×480) com a biblioteca de interface LVGL do ESPHome, em três páginas e uma barra de navegação inferior.
Personalize antes de gravar. No topo, o bloco
substitutionsdefine as entidades do seu Home Assistant (clima, ventilador, ar) e a cidade — ajuste para as suas. A chave da API é um exemplo; gere a sua. Os nomescover.cortina_1..3também são exemplos.
substitutions:
device_name: tela-clima
friendly_name: "Tela Clima"
city_name: "Fortaleza"
ha_weather_entity: "weather.home"
ha_fan_entity: "fan.ventilador_sala"
ha_ac_entity: "climate.ar_sala"
tz: "America/Fortaleza"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
platformio_options:
upload_speed: 921600
board_build.flash_mode: dio
board_build.f_flash: 80000000L
board_build.f_cpu: 240000000L
# Placa: Guition JC3248W535C_I_Y — ESP32-S3, tela QSPI 3.5" 320x480 (AXS15231B),
# toque capacitivo no mesmo chip via I2C. Confirme flash/PSRAM da sua unidade
# (16MB / PSRAM octal é o padrão dessa família; se a compilação/boot falhar,
# tente psram.mode: quad).
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
flash_size: 16MB
framework:
type: esp-idf
sdkconfig_options:
COMPILER_OPTIMIZATION_SIZE: y
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: y
CONFIG_ESP32S3_DATA_CACHE_64KB: y
CONFIG_ESP32S3_DATA_CACHE_LINE_64B: y
CONFIG_SPIRAM_FETCH_INSTRUCTIONS: y
CONFIG_SPIRAM_RODATA: y
psram:
mode: octal
speed: 80MHz
logger:
level: INFO
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
on_connect:
- lvgl.label.update:
id: wifi_icon
text: "\U000F0928"
on_disconnect:
- lvgl.label.update:
id: wifi_icon
text: "\U000F092E"
api:
encryption:
key: !secret api_encryption_key # gere a sua no ESPHome Builder
ota:
- platform: esphome
encryption:
# Necessário para a Home Assistant "empurrar" a entidade de clima para o
# dispositivo E para o dispositivo chamar serviços de volta (cortinas/ar/vent).
# Só funciona depois que o device for adicionado à integração ESPHome no HA.
time:
- platform: sntp
id: sntp_time
timezone: "${tz}"
servers:
- time.google.com
- time1.google.com
- pool.ntp.org
# --- Barramentos ---------------------------------------------------------
spi:
- type: quad
clk_pin: 47
data_pins: [21, 48, 40, 39]
i2c:
- sda: 4
scl: 8
# --- Backlight -------------------------------------------------------------
output:
- id: gpio_backlight_pwm
platform: ledc
pin: 1
light:
- id: backlight
name: "Backlight"
platform: monochromatic
output: gpio_backlight_pwm
restore_mode: ALWAYS_ON
# --- Tela + toque (AXS15231B, driver nativo mipi_spi) -----------------------
display:
- id: main_display
platform: mipi_spi
model: JC3248W535
rotation: 0° # 0° = retrato 320x480 nesta placa; troque p/ 90°/270° se vier deitada
data_rate: 40MHz
update_interval: never
auto_clear_enabled: false
touchscreen:
- id: main_touchscreen
platform: axs15231
# --- Fontes ------------------------------------------------------------
font:
- file:
type: gfonts
family: Roboto
weight: bold
id: font_time
size: 72
bpp: 4
glyphs: "0123456789:"
- file: "gfonts://Roboto"
id: font_text_md
size: 22
bpp: 4
glyphsets: GF_Latin_Core
glyphs: ["°", "•"]
- file:
type: gfonts
family: Roboto
weight: bold
id: font_num_big
size: 48
bpp: 4
glyphs: "0123456789°C+- "
# Ícones grandes do clima (sol / nuvem / chuva)
- file:
type: web
url: "https://github.com/Templarian/MaterialDesign-Webfont/raw/master/fonts/materialdesignicons-webfont.ttf"
id: font_icon_weather
size: 150
bpp: 4
glyphs:
- "\U000F0599" # weather-sunny
- "\U000F0594" # weather-night (clear-night)
- "\U000F0595" # weather-partly-cloudy
- "\U000F0590" # weather-cloudy
- "\U000F0596" # weather-pouring (chuva)
# Ícone de WiFi (pequeno)
- file:
type: web
url: "https://github.com/Templarian/MaterialDesign-Webfont/raw/master/fonts/materialdesignicons-webfont.ttf"
id: font_icon_wifi
size: 22
bpp: 4
glyphs:
- "\U000F0928" # wifi-strength-4
- "\U000F0925" # wifi-strength-3
- "\U000F0922" # wifi-strength-2
- "\U000F091F" # wifi-strength-1
- "\U000F092E" # wifi-strength-off-outline
# Ícones da barra de navegação inferior
- file:
type: web
url: "https://github.com/Templarian/MaterialDesign-Webfont/raw/master/fonts/materialdesignicons-webfont.ttf"
id: font_nav
size: 30
bpp: 4
glyphs:
- "\U000F02DC" # home
- "\U000F111C" # window-shutter (cortinas)
- "\U000F001B" # air-conditioner
# Ícones dos botões de controle
- file:
type: web
url: "https://github.com/Templarian/MaterialDesign-Webfont/raw/master/fonts/materialdesignicons-webfont.ttf"
id: font_icon
size: 40
bpp: 4
glyphs:
- "\U000F0737" # arrow-up-bold (abrir)
- "\U000F072E" # arrow-down-bold (fechar)
- "\U000F04DB" # stop (parar)
- "\U000F0425" # power
- "\U000F0210" # fan
- "\U000F001B" # air-conditioner
- "\U000F111C" # window-shutter
# --- Sensores ------------------------------------------------------------
sensor:
- platform: wifi_signal
id: wifi_signal_dbm
update_interval: 30s
internal: true
on_value:
- lvgl.label.update:
id: wifi_icon
text: !lambda |-
if (x >= -60) return std::string("\U000F0928");
else if (x >= -67) return std::string("\U000F0925");
else if (x >= -75) return std::string("\U000F0922");
else return std::string("\U000F091F");
- platform: homeassistant
id: ha_weather_temp
entity_id: ${ha_weather_entity}
attribute: temperature
internal: true
on_value:
- lvgl.label.update:
id: temp_city_label
text:
format: "${city_name} • %.0f°C"
args: [x]
if_nan: "${city_name} • --°C"
# Ar-condicionado: temperatura atual da sala
- platform: homeassistant
id: ha_ac_current
entity_id: ${ha_ac_entity}
attribute: current_temperature
internal: true
on_value:
- lvgl.label.update:
id: ac_current_label
text:
format: "Atual: %.1f°C"
args: [x]
if_nan: "Atual: --"
# Ar-condicionado: temperatura alvo (setpoint)
- platform: homeassistant
id: ha_ac_target
entity_id: ${ha_ac_entity}
attribute: temperature
internal: true
on_value:
- lvgl.label.update:
id: ac_target_label
text:
format: "%.0f°C"
args: [x]
if_nan: "--"
# Ventilador: velocidade atual (%)
- platform: homeassistant
id: ha_fan_pct
entity_id: ${ha_fan_entity}
attribute: percentage
internal: true
on_value:
- lvgl.label.update:
id: fan_speed_label
text:
format: "Velocidade: %.0f%%"
args: [x]
if_nan: "Velocidade: --"
text_sensor:
- platform: homeassistant
id: ha_weather_condition
entity_id: ${ha_weather_entity}
internal: true
on_value:
- lvgl.label.update:
id: weather_icon
text: !lambda |-
std::string c = x;
if (c == "sunny") return std::string("\U000F0599");
if (c == "clear-night") return std::string("\U000F0594");
if (c == "partlycloudy") return std::string("\U000F0595");
if (c == "rainy" || c == "pouring" || c == "lightning" ||
c == "lightning-rainy" || c == "hail" || c == "snowy" ||
c == "snowy-rainy") return std::string("\U000F0596");
return std::string("\U000F0590");
- lvgl.label.update:
id: forecast_label
text: !lambda |-
std::string c = x;
if (c == "sunny") return std::string("Ensolarado");
if (c == "clear-night") return std::string("Céu limpo");
if (c == "partlycloudy") return std::string("Parcialmente nublado");
if (c == "cloudy") return std::string("Nublado");
if (c == "fog") return std::string("Neblina");
if (c == "windy" || c == "windy-variant") return std::string("Ventania");
if (c == "rainy") return std::string("Chuva");
if (c == "pouring") return std::string("Chuva forte");
if (c == "lightning") return std::string("Tempestade");
if (c == "lightning-rainy") return std::string("Tempestade com chuva");
if (c == "hail") return std::string("Granizo");
if (c == "snowy") return std::string("Neve");
if (c == "snowy-rainy") return std::string("Chuva com neve");
return std::string("Indisponível");
# Ar-condicionado: modo atual (off/cool/dry/...) — atualiza rótulo e cor do power
- platform: homeassistant
id: ha_ac_mode
entity_id: ${ha_ac_entity}
internal: true
on_value:
- lvgl.label.update:
id: ac_mode_label
text: !lambda |-
std::string m = x;
if (m == "off") return std::string("Desligado");
if (m == "cool") return std::string("Refrigerar");
if (m == "dry") return std::string("Desumidificar");
if (m == "fan_only") return std::string("Ventilar");
if (m == "heat") return std::string("Aquecer");
if (m == "heat_cool") return std::string("Automático");
return m;
- if:
condition:
lambda: 'return x == "off";'
then:
- lvgl.widget.update:
id: btn_ac_power
bg_color: 0x263238
else:
- lvgl.widget.update:
id: btn_ac_power
bg_color: 0x2196F3
# --- Relógio / data (atualizados localmente, não dependem do HA) -----------
interval:
- interval: 1s
then:
- lvgl.label.update:
id: time_label
text: !lambda |-
auto t = id(sntp_time).now();
if (!t.is_valid()) return std::string("--:--");
char buf[8];
snprintf(buf, sizeof(buf), "%02d:%02d", t.hour, t.minute);
return std::string(buf);
- interval: 30s
then:
- lvgl.label.update:
id: date_label
text: !lambda |-
auto t = id(sntp_time).now();
if (!t.is_valid()) return std::string("Sincronizando...");
static const char *dias[] = {"", "Domingo", "Segunda-feira", "Terça-feira",
"Quarta-feira", "Quinta-feira", "Sexta-feira", "Sábado"};
static const char *meses[] = {"", "Janeiro", "Fevereiro", "Março", "Abril",
"Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"};
char buf[48];
snprintf(buf, sizeof(buf), "%s, %d de %s de %d",
dias[t.day_of_week], t.day_of_month, meses[t.month], t.year);
return std::string(buf);
# --- Interface LVGL ----------------------------------------------------
lvgl:
displays:
- main_display
touchscreens:
- main_touchscreen
bg_color: 0x000000
# Barra de navegação inferior — sempre visível sobre todas as páginas
top_layer:
widgets:
- obj:
width: 320
height: 56
align: BOTTOM_MID
y: 0
bg_color: 0x11151A
bg_opa: COVER
border_width: 0
radius: 0
pad_all: 0
- button:
id: nav_clima
width: 100
height: 48
align: BOTTOM_LEFT
x: 4
y: -4
bg_color: 0x2196F3
radius: 10
border_width: 0
widgets:
- label:
text: "\U000F02DC"
text_font: font_nav
text_color: 0xFFFFFF
align: CENTER
on_click:
- lvgl.page.show: page_clima
- lvgl.widget.update: {id: nav_clima, bg_color: 0x2196F3}
- lvgl.widget.update: {id: nav_cortinas, bg_color: 0x263238}
- lvgl.widget.update: {id: nav_ar, bg_color: 0x263238}
- button:
id: nav_cortinas
width: 100
height: 48
align: BOTTOM_LEFT
x: 110
y: -4
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label:
text: "\U000F111C"
text_font: font_nav
text_color: 0xFFFFFF
align: CENTER
on_click:
- lvgl.page.show: page_cortinas
- lvgl.widget.update: {id: nav_clima, bg_color: 0x263238}
- lvgl.widget.update: {id: nav_cortinas, bg_color: 0x2196F3}
- lvgl.widget.update: {id: nav_ar, bg_color: 0x263238}
- button:
id: nav_ar
width: 100
height: 48
align: BOTTOM_LEFT
x: 216
y: -4
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label:
text: "\U000F001B"
text_font: font_nav
text_color: 0xFFFFFF
align: CENTER
on_click:
- lvgl.page.show: page_ar
- lvgl.widget.update: {id: nav_clima, bg_color: 0x263238}
- lvgl.widget.update: {id: nav_cortinas, bg_color: 0x263238}
- lvgl.widget.update: {id: nav_ar, bg_color: 0x2196F3}
pages:
# ===================== PÁGINA 1: CLIMA / RELÓGIO =====================
- id: page_clima
bg_color: 0x000000
widgets:
- label:
id: wifi_icon
text: "\U000F092E"
text_font: font_icon_wifi
text_color: 0xB0BEC5
align: TOP_LEFT
x: 8
y: 6
- label:
id: time_label
text: "--:--"
text_font: font_time
text_color: 0xFFFFFF
align: TOP_MID
y: 4
- label:
id: date_label
text: "Sincronizando..."
text_font: font_text_md
text_color: 0xB0BEC5
align: TOP_MID
y: 98
- label:
id: weather_icon
text: "\U000F0595"
text_font: font_icon_weather
text_color: 0x90A4AE
align: CENTER
y: -20
- label:
id: forecast_label
text: "Aguardando dados..."
text_font: font_text_md
text_color: 0xFFFFFF
align: CENTER
y: 120
- label:
id: temp_city_label
text: "--"
text_font: font_text_md
text_color: 0xB0BEC5
align: CENTER
y: 155
# ===================== PÁGINA 2: CORTINAS =====================
- id: page_cortinas
bg_color: 0x000000
widgets:
- label:
text: "Cortinas"
text_font: font_text_md
text_color: 0xFFFFFF
align: TOP_MID
y: 14
# ---- Linha: Todas ----
- label:
text: "Todas"
text_font: font_text_md
text_color: 0xB0BEC5
align: TOP_LEFT
x: 12
y: 78
- button:
width: 60
height: 60
align: TOP_LEFT
x: 118
y: 60
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "\U000F0737", text_font: font_icon, text_color: 0x4CAF50, align: CENTER}
on_click:
- homeassistant.action:
action: cover.open_cover
data: {entity_id: "cover.cortina_1, cover.cortina_2, cover.cortina_3"}
- button:
width: 60
height: 60
align: TOP_LEFT
x: 186
y: 60
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "\U000F04DB", text_font: font_icon, text_color: 0xFFB300, align: CENTER}
on_click:
- homeassistant.action:
action: cover.stop_cover
data: {entity_id: "cover.cortina_1, cover.cortina_2, cover.cortina_3"}
- button:
width: 60
height: 60
align: TOP_LEFT
x: 254
y: 60
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "\U000F072E", text_font: font_icon, text_color: 0xEF5350, align: CENTER}
on_click:
- homeassistant.action:
action: cover.close_cover
data: {entity_id: "cover.cortina_1, cover.cortina_2, cover.cortina_3"}
# ---- Linha: Cortina 1 ----
- label:
text: "Cortina 1"
text_font: font_text_md
text_color: 0xFFFFFF
align: TOP_LEFT
x: 12
y: 170
- button:
width: 60
height: 60
align: TOP_LEFT
x: 118
y: 152
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "\U000F0737", text_font: font_icon, text_color: 0x4CAF50, align: CENTER}
on_click:
- homeassistant.action:
action: cover.open_cover
data: {entity_id: cover.cortina_1}
- button:
width: 60
height: 60
align: TOP_LEFT
x: 186
y: 152
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "\U000F04DB", text_font: font_icon, text_color: 0xFFB300, align: CENTER}
on_click:
- homeassistant.action:
action: cover.stop_cover
data: {entity_id: cover.cortina_1}
- button:
width: 60
height: 60
align: TOP_LEFT
x: 254
y: 152
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "\U000F072E", text_font: font_icon, text_color: 0xEF5350, align: CENTER}
on_click:
- homeassistant.action:
action: cover.close_cover
data: {entity_id: cover.cortina_1}
# ---- Linha: Cortina 2 ----
- label:
text: "Cortina 2"
text_font: font_text_md
text_color: 0xFFFFFF
align: TOP_LEFT
x: 12
y: 262
- button:
width: 60
height: 60
align: TOP_LEFT
x: 118
y: 244
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "\U000F0737", text_font: font_icon, text_color: 0x4CAF50, align: CENTER}
on_click:
- homeassistant.action:
action: cover.open_cover
data: {entity_id: cover.cortina_2}
- button:
width: 60
height: 60
align: TOP_LEFT
x: 186
y: 244
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "\U000F04DB", text_font: font_icon, text_color: 0xFFB300, align: CENTER}
on_click:
- homeassistant.action:
action: cover.stop_cover
data: {entity_id: cover.cortina_2}
- button:
width: 60
height: 60
align: TOP_LEFT
x: 254
y: 244
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "\U000F072E", text_font: font_icon, text_color: 0xEF5350, align: CENTER}
on_click:
- homeassistant.action:
action: cover.close_cover
data: {entity_id: cover.cortina_2}
# ---- Linha: Cortina 3 ----
- label:
text: "Cortina 3"
text_font: font_text_md
text_color: 0xFFFFFF
align: TOP_LEFT
x: 12
y: 354
- button:
width: 60
height: 60
align: TOP_LEFT
x: 118
y: 336
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "\U000F0737", text_font: font_icon, text_color: 0x4CAF50, align: CENTER}
on_click:
- homeassistant.action:
action: cover.open_cover
data: {entity_id: cover.cortina_3}
- button:
width: 60
height: 60
align: TOP_LEFT
x: 186
y: 336
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "\U000F04DB", text_font: font_icon, text_color: 0xFFB300, align: CENTER}
on_click:
- homeassistant.action:
action: cover.stop_cover
data: {entity_id: cover.cortina_3}
- button:
width: 60
height: 60
align: TOP_LEFT
x: 254
y: 336
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "\U000F072E", text_font: font_icon, text_color: 0xEF5350, align: CENTER}
on_click:
- homeassistant.action:
action: cover.close_cover
data: {entity_id: cover.cortina_3}
# ===================== PÁGINA 3: VENTILADOR + AR =====================
- id: page_ar
bg_color: 0x000000
widgets:
# ---------- Ventilador ----------
- label:
text: "\U000F0210"
text_font: font_icon
text_color: 0x2196F3
align: TOP_LEFT
x: 14
y: 10
- label:
text: "Ventilador"
text_font: font_text_md
text_color: 0xFFFFFF
align: TOP_LEFT
x: 62
y: 20
- button:
width: 72
height: 52
align: TOP_LEFT
x: 8
y: 62
bg_color: 0x37474F
radius: 10
border_width: 0
widgets:
- label: {text: "Desl", text_font: font_text_md, text_color: 0xFFFFFF, align: CENTER}
on_click:
- homeassistant.action:
action: fan.turn_off
data: {entity_id: "${ha_fan_entity}"}
- button:
width: 72
height: 52
align: TOP_LEFT
x: 84
y: 62
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "Baixa", text_font: font_text_md, text_color: 0xFFFFFF, align: CENTER}
on_click:
- homeassistant.action:
action: fan.set_percentage
data: {entity_id: "${ha_fan_entity}", percentage: "33"}
- button:
width: 72
height: 52
align: TOP_LEFT
x: 160
y: 62
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "Média", text_font: font_text_md, text_color: 0xFFFFFF, align: CENTER}
on_click:
- homeassistant.action:
action: fan.set_percentage
data: {entity_id: "${ha_fan_entity}", percentage: "66"}
- button:
width: 72
height: 52
align: TOP_LEFT
x: 236
y: 62
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "Alta", text_font: font_text_md, text_color: 0xFFFFFF, align: CENTER}
on_click:
- homeassistant.action:
action: fan.set_percentage
data: {entity_id: "${ha_fan_entity}", percentage: "100"}
- label:
id: fan_speed_label
text: "Velocidade: --"
text_font: font_text_md
text_color: 0x90A4AE
align: TOP_MID
y: 124
# ---------- Ar-condicionado ----------
- label:
text: "\U000F001B"
text_font: font_icon
text_color: 0x2196F3
align: TOP_LEFT
x: 14
y: 166
- label:
text: "Ar Sala"
text_font: font_text_md
text_color: 0xFFFFFF
align: TOP_LEFT
x: 62
y: 176
- button:
id: btn_ac_power
width: 60
height: 52
align: TOP_RIGHT
x: -10
y: 166
bg_color: 0x263238
radius: 10
border_width: 0
widgets:
- label: {text: "\U000F0425", text_font: font_icon, text_color: 0xFFFFFF, align: CENTER}
on_click:
- if:
condition:
lambda: 'return id(ha_ac_mode).state == "off";'
then:
- homeassistant.action:
action: climate.set_hvac_mode
data: {entity_id: "${ha_ac_entity}", hvac_mode: cool}
else:
- homeassistant.action:
action: climate.set_hvac_mode
data: {entity_id: "${ha_ac_entity}", hvac_mode: "off"}
- button:
width: 66
height: 72
align: TOP_LEFT
x: 16
y: 232
bg_color: 0x263238
radius: 12
border_width: 0
widgets:
- label: {text: "-", text_font: font_num_big, text_color: 0xFFFFFF, align: CENTER}
on_click:
- homeassistant.action:
action: climate.set_temperature
data: {entity_id: "${ha_ac_entity}"}
data_template:
temperature: "{{ [ (state_attr('${ha_ac_entity}','temperature') | float(21)) - 1, 16 ] | max }}"
- label:
id: ac_target_label
text: "--"
text_font: font_num_big
text_color: 0xFFFFFF
align: TOP_MID
y: 244
- button:
width: 66
height: 72
align: TOP_RIGHT
x: -16
y: 232
bg_color: 0x263238
radius: 12
border_width: 0
widgets:
- label: {text: "+", text_font: font_num_big, text_color: 0xFFFFFF, align: CENTER}
on_click:
- homeassistant.action:
action: climate.set_temperature
data: {entity_id: "${ha_ac_entity}"}
data_template:
temperature: "{{ [ (state_attr('${ha_ac_entity}','temperature') | float(21)) + 1, 30 ] | min }}"
- label:
id: ac_current_label
text: "Atual: --"
text_font: font_text_md
text_color: 0x90A4AE
align: TOP_MID
y: 322
- label:
id: ac_mode_label
text: "—"
text_font: font_text_md
text_color: 0x64B5F6
align: TOP_MID
y: 352
Como funciona, em resumo:
substitutionsno topo centraliza o que você troca: nome do dispositivo, entidades do HA (weather,fan,climate), cidade e fuso — sem procurar no meio do arquivo.display(drivermipi_spi, modeloJC3248W535) +touchscreen(axs15231) ativam a tela e o toque;lvgldesenha a interface em 3 páginas com barra inferior.- Sensores
platform: homeassistantpuxam dados do HA (temperatura do clima, modo do ar, velocidade do ventilador) e atualizam os rótulos; os botões chamam serviços de volta (homeassistant.action) para abrir/fechar cortinas, ligar o ar e mudar a velocidade. - O relógio e a data são gerados localmente (via
sntp), sem depender do HA. - Hardware: confirme a flash/PSRAM da sua unidade; se o boot falhar, teste
psram.mode: quad. A rotação da tela pode precisar de90°/270°.
Ajuste as substitutions para as suas entidades e valide no ESPHome Device Builder antes de gravar.
Exemplo pronto: tela Thread com temperatura e umidade (Waveshare C6-GEEK)#
A placa Waveshare ESP32-C6-GEEK junta três coisas num dispositivo só: conexão Thread, uma telinha ST7789V (240×135) e um sensor DHT (AM2302/DHT11) de temperatura e umidade. A tela mostra relógio, data e as leituras do sensor, enquanto o dispositivo entra na sua rede Thread.
Dado MUITO sensível. O campo
tlv(o dataset) do Thread contém a chave da sua rede — nunca o publique. A chave da API também é um exemplo; gere a sua.
# Waveshare ESP32-C6-GEEK: dispositivo Thread com tela ST7789V e sensor DHT.
# Troque a chave da API e, principalmente, o dataset TLV da sua rede Thread.
esphome:
name: waveshare-esp32-c6-geek
friendly_name: "Waveshare ESP32-C6-GEEK"
min_version: 2026.9.0
esp32:
board: esp32-c6-devkitc-1
variant: esp32c6
flash_size: 16MB
framework:
type: esp-idf
version: recommended
sdkconfig_options:
CONFIG_ESP_TASK_WDT_TIMEOUT_S: "60" # watchdog maior p/ FTD (init pode demorar)
CONFIG_PM_ENABLE: "n"
CONFIG_ESP_INT_WDT_TIMEOUT_MS: "2000"
logger:
level: DEBUG
api:
encryption:
key: !secret api_encryption_key # gere a sua no ESPHome Builder
# Mantém a tela funcionando mesmo sem conexão com o HA.
reboot_timeout: 0s
ota:
- platform: esphome
encryption:
# Thread usa IPv6.
network:
enable_ipv6: true
# Rádio Thread. ATENÇÃO: o 'tlv' (dataset) contém a CHAVE DA SUA REDE THREAD.
# Nunca publique o seu; cole o dataset da sua rede aqui.
openthread:
device_type: FTD
force_dataset: true
tlv: "COLE_AQUI_O_DATASET_TLV_DA_SUA_REDE_THREAD"
output_power: 20dBm
# Horário do Google, exibido no fuso de Fortaleza.
time:
- platform: sntp
id: horario_google
timezone: America/Fortaleza
servers:
- time1.google.com
- time2.google.com
- time3.google.com
update_interval: 1h
spi:
clk_pin: GPIO1
mosi_pin: GPIO2
# As fontes são baixadas durante a preparação do firmware.
font:
- file: "gfonts://Roboto"
id: fonte_pequena
size: 14
- file: "gfonts://Roboto"
id: fonte_valores
size: 26
glyphs: ["0123456789 .-°C%"]
- file: "gfonts://Roboto"
id: fonte_relogio
size: 28
sensor:
- platform: dht
id: sensor_dht_1
pin: GPIO11
model: AM2302
update_interval: 10s
temperature:
name: Temperature
id: sensor_dht_1_temperature
icon: "mdi:thermometer"
accuracy_decimals: 0
humidity:
name: Humidity
id: sensor_dht_1_humidity
icon: "mdi:water-percent"
accuracy_decimals: 0
- platform: uptime
name: "Waveshare ESP32-C6-GEEK Tempo de Atividade"
id: uptime_sensor
update_interval: 60s
filters:
- lambda: return x / 60.0;
unit_of_measurement: "min"
icon: mdi:clock-outline
display:
- platform: st7789v
id: tela
model: CUSTOM
cs_pin: GPIO5
dc_pin: GPIO3
reset_pin: GPIO4
backlight_pin: GPIO6
# Geometria nativa; a rotação resulta em 240 × 135.
width: 135
height: 240
offset_width: 52
offset_height: 40
rotation: 270
update_interval: 1s
lambda: |-
const Color branco(255, 255, 255);
const Color cinza(160, 160, 160);
const Color azul(80, 190, 255);
const Color laranja(255, 180, 70);
it.fill(Color(0, 0, 0));
// Hora e data.
auto agora = id(horario_google).now();
if (agora.is_valid()) {
it.strftime(
120, 0, id(fonte_relogio), branco,
TextAlign::TOP_CENTER, "%H:%M:%S", agora
);
it.strftime(
120, 34, id(fonte_pequena), cinza,
TextAlign::TOP_CENTER, "%d/%m/%Y", agora
);
} else {
it.print(
120, 0, id(fonte_relogio), branco,
TextAlign::TOP_CENTER, "--:--:--"
);
it.print(
120, 34, id(fonte_pequena), cinza,
TextAlign::TOP_CENTER, "Sincronizando hora..."
);
}
// Separadores e títulos.
it.line(8, 57, 231, 57, cinza);
it.line(120, 67, 120, 125, cinza);
it.print(
60, 65, id(fonte_pequena), cinza,
TextAlign::TOP_CENTER, "Temperatura"
);
it.print(
180, 65, id(fonte_pequena), cinza,
TextAlign::TOP_CENTER, "Umidade"
);
// Leituras do DHT.
const float temperatura =
id(sensor_dht_1_temperature).state;
const float umidade =
id(sensor_dht_1_humidity).state;
if (!isnan(temperatura)) {
it.printf(
60, 88, id(fonte_valores), laranja,
TextAlign::TOP_CENTER, "%.0f °C", temperatura
);
} else {
it.print(
60, 88, id(fonte_valores), laranja,
TextAlign::TOP_CENTER, "-- °C"
);
}
if (!isnan(umidade)) {
it.printf(
180, 88, id(fonte_valores), azul,
TextAlign::TOP_CENTER, "%.0f %%", umidade
);
} else {
it.print(
180, 88, id(fonte_valores), azul,
TextAlign::TOP_CENTER, "-- %"
);
}
switch:
- platform: restart
id: restart_switch
name: "Waveshare ESP32-C6-GEEK Restart Switch"
binary_sensor:
- platform: status
name: "Waveshare ESP32-C6-GEEK Status de Conexão"
id: status_conexao
text_sensor:
- platform: openthread_info
ip_address:
name: "Thread IP Address"
channel:
name: "Thread Channel"
role:
name: "Thread Device Role"
rloc16:
name: "Thread RLOC16"
ext_addr:
name: "Thread Extended Address"
eui64:
name: "Thread EUI64"
network_name:
name: "Thread Network Name"
network_key:
name: "Thread Network Key"
pan_id:
name: "Thread PAN ID"
ext_pan_id:
name: "Thread Extended PAN ID"
Como funciona, em resumo:
openthread(FTD) conecta a placa à rede Thread; otlvé o dataset sigiloso (contém a chave da rede). Onetwork: enable_ipv6é obrigatório.- Um sensor DHT no
GPIO11lê temperatura e umidade a cada 10 s. - O
displayST7789V desenha, nolambda, o relógio (viasntp), a data e as duas leituras — tudo localmente, sem depender do HA para a tela (reboot_timeout: 0smantém a tela viva mesmo sem o HA). - Sensores de diagnóstico (uptime, status, dados do Thread) ajudam a monitorar.
Confira o sensor e a pinagem da sua placa, e valide no ESPHome Device Builder antes de gravar.
Exemplo de referência: painel de parede 7" para a casa toda (Waveshare 7B)#
Um painel de parede de 7" (Waveshare ESP32-S3-Touch-LCD-7B, 1024×600) que vira o controle central da casa: mostra relógio, clima com previsão de 5 dias, cotação do Bitcoin (com mini-gráfico) e uma página por cômodo com luzes (liga/desliga + brilho), ar-condicionado, ventilador e cortinas — tudo via Home Assistant, com LVGL v9.
Por ser grande e depender de arquivos próprios, vai aqui como referência comentada (trechos-chave), não o arquivo inteiro.
Este exemplo NÃO é autossuficiente. Ele precisa acompanhar: -
waveshare_7b/panel_data.h— um header C++ com a lógica do Bitcoin (parsing da Coinbase, mini-gráfico) e a funçãocondition_pt(), que traduz as condições do clima; - as imagens dos cômodos (waveshare_7b/assets/*.jpg).Requer ESPHome 2026.9+ e, no HA, a opção "Permitir que o dispositivo realize ações do Home Assistant" ativada. A chave da API e o IP fixo são apenas exemplos — gere/ajuste os seus.
1. Base (placa, PSRAM, API e Wi-Fi):
substitutions:
name: waveshare-esp32-s3-touch-lcd-7b
friendly_name: Waveshare ESP32-S3-Touch-LCD-7B
weather_entity: weather.home
esphome:
name: ${name}
friendly_name: ${friendly_name}
min_version: 2026.9.0
includes:
- waveshare_7b/panel_data.h # header C++ (lógica do Bitcoin, condition_pt) — precisa acompanhar
on_boot:
- priority: -100
then:
- light.turn_on: { id: screen_backlight, brightness: 75% }
- script.execute: refresh_clock
- delay: 8s
- script.execute: fetch_market
- script.execute: fetch_forecast
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
flash_size: 16MB
framework:
type: esp-idf
psram:
mode: octal
speed: 80MHz
api:
encryption:
key: !secret api_encryption_key # gere a sua no ESPHome Builder
reboot_timeout: 0s # mantém a tela viva mesmo sem o HA
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# manual_ip opcional — ajuste à sua rede ou remova para usar DHCP
2. Tela RGB + toque (a parte mais sensível do hardware):
# Tela RGB 1024x600 (driver nativo mipi_rgb) + toque GT911 no barramento I2C.
# Um expansor I2C (waveshare_io_ch32v003) controla reset/enable/backlight.
display:
- platform: mipi_rgb
id: disp1
model: RPI
dimensions: { width: 1024, height: 600 }
color_order: RGB
pclk_frequency: 30MHz
pclk_inverted: true
# pinos de dados e sincronismo conforme a placa (ver config completa)
update_interval: never
auto_clear_enabled: false
touchscreen:
- platform: gt911
id: touch_panel
display: disp1
i2c_id: touch_bus
address: 93
3. Cotação do Bitcoin (requisição HTTP à Coinbase, sem depender do HA):
http_request:
id: market_http
verify_ssl: true
script:
- id: fetch_market
then:
- http_request.get:
url: https://api.exchange.coinbase.com/products/BTC-USD/ticker
capture_response: true
on_response:
- lambda: |-
waveshare_panel::bitcoin().parse_quote(body, id(panel_time).now().strftime("%H:%M"));
- script.execute: refresh_bitcoin
4. Padrão de controle por entidade (repetido para cada luz/cômodo):
# Um sensor lê o brilho da luz no HA e move o slider da tela:
sensor:
- platform: homeassistant
id: level_light_led_sala
entity_id: light.led_sala
attribute: brightness
on_value:
- lvgl.slider.update:
id: slider_light_led_sala
value: !lambda "return std::isfinite(x) ? (int)lroundf(x * 100.0f / 255.0f) : 0;"
# E, dentro da página do cômodo, o botão/slider chama o serviço de volta:
# button (toggle) + slider (brilho) -> homeassistant.action: light.toggle / light.turn_on
Como funciona, em resumo:
- A tela usa o driver nativo
mipi_rgb(1024×600) e o toquegt911; um expansor I²C (waveshare_io_ch32v003) cuida de reset, enable e backlight. - Dezenas de sensores
platform: homeassistantespelham o estado das entidades (brilho, modo do ar, estado das cortinas) e atualizam os widgets LVGL; os botões e sliders chamam serviços de volta comhomeassistant.action. - O relógio, a previsão (via
weather.get_forecastsdo HA) e o Bitcoin (API da Coinbase) são atualizados porinterval/script. - A interface tem uma página principal (clima + BTC + atalhos por cômodo) e uma página por cômodo, com rolagem.
Trate este exemplo como um ponto de partida avançado: o arquivo completo (com panel_data.h e as imagens) é extenso e específico da casa.
Exemplo pronto: assistente de voz com tela (M5Stack AtomS3R + Echo Base)#
Um assistente de voz com telinha de status, feito com o M5Stack AtomS3R + Echo Base. Ele detecta palavras de ativação (Okay Nabu, Hey Jarvis, Hey Mycroft) com wake word no próprio dispositivo (ou pelo Home Assistant) e conversa com o Assist do HA; a tela mostra cada fase — ouvindo, pensando, respondendo — com as ilustrações do Casita.
Troque pelos seus dados antes de gravar. A chave da API e o IP fixo são exemplos; gere/ajuste os seus. As ilustrações, sons e modelos de wake word são baixados de URLs públicas durante a compilação — este exemplo não precisa de arquivos locais. Requer ESPHome 2026.7+ e o hardware de áudio do Echo Base (microfone + alto-falante).
substitutions:
name: atom-lcd-echo
friendly_name: "ATOM LCD ECHO"
# Imagens do Casita
loading_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/loading_320_240.png
idle_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/idle_320_240.png
listening_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/listening_320_240.png
thinking_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/thinking_320_240.png
replying_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/replying_320_240.png
error_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/error_320_240.png
error_no_wifi_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-wifi.png
error_no_ha_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-ha.png
# Fontes
mdi_webfont_file: https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/fonts/materialdesignicons-webfont.ttf
# Arquivos de áudio
wake_word_trigger_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/wake_word_triggered.flac
timer_finished_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac
error_cloud_expired_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/error_cloud_expired.mp3
# Modelos Micro Wake Word
stop_model_file: https://github.com/kahrendt/microWakeWord/releases/download/stop/stop.json
# Cores de fundo
loading_illustration_background_color: "000000"
idle_illustration_background_color: "000000"
listening_illustration_background_color: "FFFFFF"
thinking_illustration_background_color: "FFFFFF"
replying_illustration_background_color: "FFFFFF"
error_illustration_background_color: "000000"
# Fases do assistente de voz
voice_assist_idle_phase_id: "1"
voice_assist_listening_phase_id: "2"
voice_assist_thinking_phase_id: "3"
voice_assist_replying_phase_id: "4"
voice_assist_not_ready_phase_id: "10"
voice_assist_error_phase_id: "11"
voice_assist_muted_phase_id: "12"
voice_assist_timer_finished_phase_id: "20"
esphome:
name: ${name}
friendly_name: ${friendly_name}
min_version: 2026.7.0
on_boot:
priority: 200
then:
- light.turn_on:
id: lcd_backlight
brightness: 100%
- delay: 500ms
- component.update: atoms3r_lcd
- script.execute: draw_display
- delay: 30s
- if:
condition:
lambda: return id(init_in_progress);
then:
- lambda: id(init_in_progress) = false;
- script.execute: draw_display
esp32:
variant: ESP32S3
board: esp32-s3-devkitc-1
flash_size: 8MB
cpu_frequency: 240MHz
framework:
type: esp-idf
sdkconfig_options:
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
psram:
mode: octal
speed: 80MHz
api:
encryption:
key: !secret api_encryption_key # gere a sua no ESPHome Builder
on_client_connected:
- script.execute: draw_display
on_client_disconnected:
- script.execute: draw_display
ota:
- platform: esphome
id: ota_esphome
encryption:
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
manual_ip: # ajuste à sua rede ou remova para DHCP
static_ip: 192.168.1.60
gateway: 192.168.1.1
subnet: 255.255.255.0
dns1: 8.8.8.8
dns2: 8.8.4.4
power_save_mode: NONE
on_connect:
- script.execute: draw_display
on_disconnect:
then:
- script.execute: draw_display
logger:
level: DEBUG
hardware_uart: USB_SERIAL_JTAG
button:
- platform: factory_reset
id: factory_reset_btn
internal: true
- platform: template
name: "Testar alto-falante"
entity_category: diagnostic
on_press:
- switch.turn_on: speaker_enable
- script.execute:
id: play_sound
priority: true
sound_file: !lambda return id(wake_word_triggered_sound);
binary_sensor:
- platform: gpio
pin:
number: GPIO41
mode: INPUT_PULLUP
inverted: true
id: user_button
internal: true
on_multi_click:
# Clique rápido alterna o mudo
- timing:
- ON for at least 50ms
- OFF for at least 50ms
then:
- switch.toggle: mute
- switch.turn_off: timer_ringing
# Pressionar por 10 segundos restaura os padrões de fábrica
- timing:
- ON for at least 10s
then:
- button.press: factory_reset_btn
external_components:
- source: github://m5stack/esphome-yaml/components@main
components:
- lp5562
- es8311
refresh: 1d
i2c:
- id: bsp_bus
sda:
number: GPIO45
ignore_strapping_warning: true
scl:
number: GPIO0
ignore_strapping_warning: true
scan: true
- id: ext_bus
sda: GPIO38
scl: GPIO39
scan: true
pi4ioe5v6408:
- id: pi4ioe5v6408_hub
i2c_id: ext_bus
address: 0x43
i2s_audio:
- id: i2s_audio_bus
i2s_lrclk_pin: GPIO6
i2s_bclk_pin: GPIO8
spi:
id: spi_bus
clk_pin: GPIO15
mosi_pin: GPIO21
audio_dac:
- platform: es8311
id: es8311_dac
i2c_id: ext_bus
bits_per_sample: 16bit
sample_rate: 48000
use_microphone: false
use_mclk: false
microphone:
- platform: i2s_audio
id: i2s_mic
sample_rate: 16000
i2s_din_pin: GPIO7
bits_per_sample: 16bit
adc_type: external
speaker:
- platform: i2s_audio
id: i2s_speaker
i2s_dout_pin: GPIO5
dac_type: external
sample_rate: 48000
audio_dac: es8311_dac
channel: left
media_player:
- platform: speaker
name: "Echo Base Player"
id: echo_base_player
volume_min: 0.5
volume_max: 0.8
announcement_pipeline:
speaker: i2s_speaker
format: FLAC
sample_rate: 48000
num_channels: 1
files:
- id: wake_word_triggered_sound
file: ${wake_word_trigger_sound_file}
- id: timer_finished_sound
file: ${timer_finished_sound_file}
- id: error_cloud_expired
file: ${error_cloud_expired_file}
on_state:
- logger.log: "Estado do media player atualizado"
on_play:
- logger.log: "Reprodução iniciada"
on_announcement:
then:
- logger.log:
format: "Reproduzindo anúncio"
level: DEBUG
- if:
condition:
microphone.is_capturing:
then:
- script.execute: stop_wake_word
- if:
condition:
lambda: |-
return id(wake_word_engine_location).current_option() ==
"In Home Assistant";
then:
- wait_until:
not:
voice_assistant.is_running:
- if:
condition:
not:
voice_assistant.is_running:
then:
- lambda: |-
id(voice_assistant_phase) =
${voice_assist_muted_phase_id};
- script.execute: draw_display
on_idle:
- if:
condition:
not:
voice_assistant.is_running:
then:
- script.execute: start_wake_word
- script.execute: set_idle_or_mute_phase
- script.execute: draw_display
switch:
# Habilitação do amplificador NS4150B
- platform: gpio
name: "Speaker Enable"
id: speaker_enable
pin:
pi4ioe5v6408: pi4ioe5v6408_hub
number: 0
mode:
output: true
icon: "mdi:volume-high"
restore_mode: ALWAYS_ON
- platform: template
name: "Mute Microphone"
id: mute
icon: "mdi:microphone-off"
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
entity_category: config
on_turn_off:
- microphone.unmute:
- lambda: |-
id(voice_assistant_phase) =
${voice_assist_idle_phase_id};
- script.execute: draw_display
on_turn_on:
- microphone.mute:
- lambda: |-
id(voice_assistant_phase) =
${voice_assist_muted_phase_id};
- script.execute: draw_display
- platform: template
id: timer_ringing
optimistic: true
internal: true
restore_mode: ALWAYS_OFF
on_turn_off:
- lambda: |-
id(echo_base_player)
->make_call()
.set_command(
media_player::MediaPlayerCommand::
MEDIA_PLAYER_COMMAND_REPEAT_OFF
)
.set_announcement(true)
.perform();
id(echo_base_player)->set_playlist_delay_ms(
speaker::AudioPipelineType::ANNOUNCEMENT,
0
);
- media_player.stop:
id: echo_base_player
announcement: true
- script.execute: start_wake_word
on_turn_on:
- script.execute: stop_wake_word
- lambda: |-
id(echo_base_player)
->make_call()
.set_command(
media_player::MediaPlayerCommand::
MEDIA_PLAYER_COMMAND_REPEAT_ONE
)
.set_announcement(true)
.perform();
id(echo_base_player)->set_playlist_delay_ms(
speaker::AudioPipelineType::ANNOUNCEMENT,
1000
);
- script.execute:
id: play_sound
priority: true
sound_file: !lambda return id(timer_finished_sound);
- delay: 15min
- switch.turn_off: timer_ringing
select:
- platform: template
entity_category: config
name: "Wake word engine location"
id: wake_word_engine_location
icon: "mdi:account-voice"
optimistic: true
restore_value: true
options:
- In Home Assistant
- On device
initial_option: On device
on_value:
- if:
condition:
lambda: return !id(init_in_progress);
then:
- wait_until:
lambda: |-
return
id(voice_assistant_phase) ==
${voice_assist_muted_phase_id} ||
id(voice_assistant_phase) ==
${voice_assist_idle_phase_id};
- if:
condition:
lambda: return x == "In Home Assistant";
then:
- micro_wake_word.stop:
- delay: 500ms
- if:
condition:
switch.is_off: mute
then:
- lambda: id(va).set_use_wake_word(true);
- voice_assistant.start_continuous:
- if:
condition:
lambda: return x == "On device";
then:
- lambda: id(va).set_use_wake_word(false);
- voice_assistant.stop:
- delay: 500ms
- if:
condition:
switch.is_off: mute
then:
- micro_wake_word.start:
- platform: template
name: "Wake word sensitivity"
id: wake_word_sensitivity
optimistic: true
initial_option: Slightly sensitive
restore_value: true
entity_category: config
options:
- Slightly sensitive
- Moderately sensitive
- Very sensitive
on_value:
- lambda: |-
if (x == "Slightly sensitive") {
id(okay_nabu).set_probability_cutoff(217);
id(hey_jarvis).set_probability_cutoff(247);
id(hey_mycroft).set_probability_cutoff(253);
} else if (x == "Moderately sensitive") {
id(okay_nabu).set_probability_cutoff(176);
id(hey_jarvis).set_probability_cutoff(235);
id(hey_mycroft).set_probability_cutoff(242);
} else if (x == "Very sensitive") {
id(okay_nabu).set_probability_cutoff(143);
id(hey_jarvis).set_probability_cutoff(212);
id(hey_mycroft).set_probability_cutoff(237);
}
micro_wake_word:
id: mww
microphone: i2s_mic
models:
- model: okay_nabu
id: okay_nabu
- model: hey_jarvis
id: hey_jarvis
- model: hey_mycroft
id: hey_mycroft
- model: ${stop_model_file}
id: stop
internal: true
vad:
on_wake_word_detected:
- script.execute:
id: play_sound
priority: true
sound_file: !lambda return id(wake_word_triggered_sound);
- wait_until:
condition:
media_player.is_announcing: echo_base_player
timeout: 500ms
- wait_until:
condition:
and:
- not:
media_player.is_announcing: echo_base_player
- not:
speaker.is_playing: i2s_speaker
timeout: 10s
- voice_assistant.start:
wake_word: !lambda return wake_word;
voice_assistant:
id: va
microphone: i2s_mic
media_player: echo_base_player
micro_wake_word: mww
noise_suppression_level: 2
auto_gain: 31dBFS
volume_multiplier: 2.0
on_listening:
- lambda: |-
id(voice_assistant_phase) =
${voice_assist_listening_phase_id};
- script.execute: draw_display
on_stt_vad_end:
- lambda: |-
id(voice_assistant_phase) =
${voice_assist_thinking_phase_id};
- script.execute: draw_display
on_tts_start:
- lambda: |-
id(voice_assistant_phase) =
${voice_assist_replying_phase_id};
- script.execute: draw_display
on_end:
- wait_until:
condition:
media_player.is_announcing: echo_base_player
timeout: 500ms
- wait_until:
condition:
and:
- not:
media_player.is_announcing: echo_base_player
- not:
speaker.is_playing: i2s_speaker
timeout: 30s
- if:
condition:
lambda: |-
return id(wake_word_engine_location).current_option() ==
"On device";
then:
- lambda: id(va).set_use_wake_word(false);
- micro_wake_word.start:
- script.execute: set_idle_or_mute_phase
- script.execute: draw_display
on_error:
- if:
condition:
and:
- lambda: return !id(init_in_progress);
- lambda: return code != "duplicate_wake_up_detected";
- lambda: return code != "stt-no-text-recognized";
then:
- lambda: |-
id(voice_assistant_phase) =
${voice_assist_error_phase_id};
- script.execute: draw_display
- delay: 1s
- script.execute: set_idle_or_mute_phase
- script.execute: draw_display
- if:
condition:
lambda: return code == "cloud-auth-failed";
then:
- script.execute:
id: play_sound
priority: true
sound_file: !lambda return id(error_cloud_expired);
- script.execute: draw_display
on_client_connected:
- lambda: id(init_in_progress) = false;
- script.execute: start_wake_word
- script.execute: set_idle_or_mute_phase
- script.execute: draw_display
on_client_disconnected:
- script.execute: stop_wake_word
- lambda: |-
id(voice_assistant_phase) =
${voice_assist_not_ready_phase_id};
- script.execute: draw_display
on_timer_finished:
- switch.turn_on: timer_ringing
- wait_until:
condition:
media_player.is_announcing: echo_base_player
timeout: 5s
- lambda: |-
id(voice_assistant_phase) =
${voice_assist_timer_finished_phase_id};
- script.execute: draw_display
globals:
- id: init_in_progress
type: bool
restore_value: false
initial_value: "true"
- id: voice_assistant_phase
type: int
restore_value: false
initial_value: ${voice_assist_not_ready_phase_id}
lp5562:
id: lp5562_led
i2c_id: bsp_bus
use_internal_clk: true
white_current: 17.5
output:
- platform: lp5562
id: lp5562_white_channel
lp5562_id: lp5562_led
channel: white
light:
- platform: monochromatic
name: "LCD Backlight"
id: lcd_backlight
output: lp5562_white_channel
icon: "mdi:television"
restore_mode: ALWAYS_ON
time:
- platform: sntp
id: sntp_time
servers:
- time.google.com
timezone: "America/Fortaleza"
on_time_sync:
- script.execute: draw_display
font:
- file: "gfonts://Roboto"
id: time_font
size: 18
- file: "gfonts://Roboto"
id: status_font
size: 16
- file: "gfonts://Roboto@700"
id: time_bold_font
size: 23
- file: "gfonts://Roboto@400"
id: date_font
size: 14
- file: "gfonts://Roboto@700"
id: pronto_font
size: 19
- file: ${mdi_webfont_file}
id: mdi_icon_128
size: 128
bpp: 4
glyphs:
- "\U000F036D" # mdi:mic-mute
color:
- id: idle_color
hex: ${idle_illustration_background_color}
- id: listening_color
hex: ${listening_illustration_background_color}
- id: thinking_color
hex: ${thinking_illustration_background_color}
- id: replying_color
hex: ${replying_illustration_background_color}
- id: loading_color
hex: ${loading_illustration_background_color}
- id: error_color
hex: ${error_illustration_background_color}
- id: text_yellow
red: 50%
green: 50%
blue: 0%
- id: text_cyan
red: 0%
green: 50%
blue: 50%
- id: text_green
red: 0%
green: 100%
blue: 0%
- id: text_red
red: 100%
green: 0%
blue: 0%
- id: light_gray
hex: "CCCCCC"
- id: text_blue
hex: "2196F3"
- id: color_white
hex: "FFFFFF"
- id: color_black
hex: "000000"
image:
- file: ${error_illustration_file}
id: casita_error
resize: 160x120
type: RGB
transparency: alpha_channel
- file: ${idle_illustration_file}
id: casita_idle
resize: 160x120
type: RGB
transparency: alpha_channel
- file: ${listening_illustration_file}
id: casita_listening
resize: 160x120
type: RGB
transparency: alpha_channel
- file: ${thinking_illustration_file}
id: casita_thinking
resize: 160x120
type: RGB
transparency: alpha_channel
- file: ${replying_illustration_file}
id: casita_replying
resize: 160x120
type: RGB
transparency: alpha_channel
- file: ${loading_illustration_file}
id: casita_initializing
resize: 160x120
type: RGB
transparency: alpha_channel
- file: ${error_no_wifi_illustration_file}
id: error_no_wifi
resize: 160x120
type: RGB
transparency: alpha_channel
- file: ${error_no_ha_illustration_file}
id: error_no_ha
resize: 160x120
type: RGB
transparency: alpha_channel
display:
- platform: mipi_spi
id: atoms3r_lcd
model: M5STACK-ATOMS3R-GC9107
spi_id: spi_bus
rotation: 180°
update_interval: never
pages:
- id: idle_page
lambda: |-
it.fill(id(idle_color));
it.image(64, 64, id(casita_idle), ImageAlign::CENTER);
it.strftime(64, 6, id(time_bold_font), id(color_white), TextAlign::CENTER, "%H:%M", id(sntp_time).now());
it.strftime(64, 32, id(date_font), id(light_gray), TextAlign::CENTER, "%d/%m/%Y", id(sntp_time).now());
it.printf(64, 96, id(pronto_font), id(text_blue), TextAlign::CENTER, "PRONTO!");
- id: listening_page
lambda: |-
it.fill(id(listening_color));
it.image(64, 64, id(casita_listening), ImageAlign::CENTER);
it.strftime(64, 8, id(time_font), id(color_white), TextAlign::CENTER, "%H:%M", id(sntp_time).now());
it.strftime(64, 26, id(time_font), id(color_white), TextAlign::CENTER, "%d/%m/%Y", id(sntp_time).now());
it.printf(64, 92, id(status_font), id(text_yellow), TextAlign::CENTER, "Ouvindo...");
- id: thinking_page
lambda: |-
it.fill(id(thinking_color));
it.image(64, 64, id(casita_thinking), ImageAlign::CENTER);
it.strftime(64, 8, id(time_font), id(color_white), TextAlign::CENTER, "%H:%M", id(sntp_time).now());
it.strftime(64, 26, id(time_font), id(color_white), TextAlign::CENTER, "%d/%m/%Y", id(sntp_time).now());
it.printf(64, 92, id(status_font), id(text_cyan), TextAlign::CENTER, "Enviando p/ HA...");
- id: replying_page
lambda: |-
it.fill(id(replying_color));
it.image(64, 64, id(casita_replying), ImageAlign::CENTER);
it.strftime(64, 8, id(time_font), id(color_white), TextAlign::CENTER, "%H:%M", id(sntp_time).now());
it.strftime(64, 26, id(time_font), id(color_white), TextAlign::CENTER, "%d/%m/%Y", id(sntp_time).now());
it.printf(64, 92, id(status_font), id(text_green), TextAlign::CENTER, "Respondendo...");
- id: error_page
lambda: |-
it.fill(id(error_color));
it.image(64, 64, id(casita_error), ImageAlign::CENTER);
it.strftime(64, 8, id(time_font), id(color_white), TextAlign::CENTER, "%H:%M", id(sntp_time).now());
it.strftime(64, 26, id(time_font), id(color_white), TextAlign::CENTER, "%d/%m/%Y", id(sntp_time).now());
it.printf(64, 92, id(status_font), id(text_red), TextAlign::CENTER, "Erro");
- id: no_ha_page
lambda: |-
it.fill(id(error_color));
it.image(64, 64, id(error_no_ha), ImageAlign::CENTER);
it.strftime(64, 8, id(time_font), id(color_white), TextAlign::CENTER, "%H:%M", id(sntp_time).now());
it.strftime(64, 26, id(time_font), id(color_white), TextAlign::CENTER, "%d/%m/%Y", id(sntp_time).now());
it.printf(64, 100, id(status_font), id(text_red), TextAlign::CENTER, "Sem conexão HA");
- id: no_wifi_page
lambda: |-
it.fill(id(error_color));
it.image(64, 64, id(error_no_wifi), ImageAlign::CENTER);
it.strftime(64, 8, id(time_font), id(color_white), TextAlign::CENTER, "%H:%M", id(sntp_time).now());
it.strftime(64, 26, id(time_font), id(color_white), TextAlign::CENTER, "%d/%m/%Y", id(sntp_time).now());
it.printf(64, 100, id(status_font), id(text_red), TextAlign::CENTER, "Sem Wi-Fi");
- id: initializing_page
lambda: |-
it.fill(id(loading_color));
it.image(64, 64, id(casita_initializing), ImageAlign::CENTER);
it.strftime(64, 8, id(time_font), id(color_white), TextAlign::CENTER, "%H:%M", id(sntp_time).now());
it.strftime(64, 26, id(time_font), id(color_white), TextAlign::CENTER, "%d/%m/%Y", id(sntp_time).now());
it.printf(64, 92, id(status_font), id(color_white), TextAlign::CENTER, "Inicializando...");
- id: muted_page
lambda: |-
it.fill(id(color_black));
it.image(64, 64, id(casita_idle), ImageAlign::CENTER);
it.strftime(64, 8, id(time_font), id(color_white), TextAlign::CENTER, "%H:%M", id(sntp_time).now());
it.strftime(64, 26, id(time_font), id(color_white), TextAlign::CENTER, "%d/%m/%Y", id(sntp_time).now());
it.printf(64, 92, id(status_font), id(text_red), TextAlign::CENTER, "Microfone mudo");
script:
- id: start_wake_word
mode: restart
then:
- if:
condition:
and:
- switch.is_off: mute
- not:
voice_assistant.is_running:
- lambda: |-
return
id(wake_word_engine_location).current_option() ==
"On device";
then:
- lambda: id(va).set_use_wake_word(false);
- micro_wake_word.start:
- if:
condition:
and:
- switch.is_off: mute
- not:
voice_assistant.is_running:
- lambda: |-
return
id(wake_word_engine_location).current_option() ==
"In Home Assistant";
then:
- lambda: id(va).set_use_wake_word(true);
- voice_assistant.start_continuous:
- id: stop_wake_word
mode: restart
then:
- if:
condition:
lambda: |-
return
id(wake_word_engine_location).current_option() ==
"In Home Assistant";
then:
- lambda: id(va).set_use_wake_word(false);
- voice_assistant.stop:
- if:
condition:
lambda: |-
return
id(wake_word_engine_location).current_option() ==
"On device";
then:
- micro_wake_word.stop:
- id: set_idle_or_mute_phase
mode: restart
then:
- if:
condition:
switch.is_off: mute
then:
- lambda: |-
id(voice_assistant_phase) =
${voice_assist_idle_phase_id};
else:
- lambda: |-
id(voice_assistant_phase) =
${voice_assist_muted_phase_id};
- id: play_sound
mode: restart
parameters:
priority: bool
sound_file: "audio::AudioFile*"
then:
- lambda: |-
if (priority) {
id(echo_base_player)
->make_call()
.set_command(
media_player::MediaPlayerCommand::
MEDIA_PLAYER_COMMAND_STOP
)
.set_announcement(true)
.perform();
}
if (
id(echo_base_player).state !=
media_player::MediaPlayerState::
MEDIA_PLAYER_STATE_ANNOUNCING ||
priority
) {
id(echo_base_player)->play_file(
sound_file,
true,
false
);
}
- id: draw_display
mode: restart
then:
- if:
condition:
lambda: return !id(init_in_progress);
then:
- if:
condition:
wifi.connected:
then:
- if:
condition:
api.connected:
then:
- lambda: |-
switch (id(voice_assistant_phase)) {
case ${voice_assist_listening_phase_id}:
id(atoms3r_lcd).show_page(listening_page);
break;
case ${voice_assist_thinking_phase_id}:
id(atoms3r_lcd).show_page(thinking_page);
break;
case ${voice_assist_replying_phase_id}:
id(atoms3r_lcd).show_page(replying_page);
break;
case ${voice_assist_error_phase_id}:
id(atoms3r_lcd).show_page(error_page);
break;
case ${voice_assist_muted_phase_id}:
id(atoms3r_lcd).show_page(muted_page);
break;
case ${voice_assist_not_ready_phase_id}:
id(atoms3r_lcd).show_page(no_ha_page);
break;
default:
id(atoms3r_lcd).show_page(idle_page);
break;
}
id(atoms3r_lcd).update();
else:
- display.page.show: no_ha_page
- component.update: atoms3r_lcd
else:
- display.page.show: no_wifi_page
- component.update: atoms3r_lcd
else:
- display.page.show: initializing_page
- component.update: atoms3r_lcd
interval:
# Atualiza o relógio da tela sem redesenhar excessivamente
- interval: 30s
then:
- if:
condition:
lambda: |-
return
!id(init_in_progress) &&
id(voice_assistant_phase) ==
${voice_assist_idle_phase_id};
then:
- script.execute: draw_display
text_sensor:
- platform: wifi_info
ip_address:
name: "IP"
dns_address:
name: "DNS"
mac_address:
name: "MAC"
Como funciona, em resumo:
- O
micro_wake_wordroda os modelos localmente (okay_nabu, hey_jarvis, hey_mycroft). Ao detectar a palavra, ovoice_assistantinicia e conversa com o Assist do Home Assistant. - A tela (
mipi_spi, AtomS3R GC9107) tem uma página por fase (ocioso, ouvindo, pensando, respondendo, erro, mudo, sem Wi-Fi, sem HA); o scriptdraw_displayescolhe a página conforme o estado. - O botão físico: clique curto muda/desmuta o microfone; segurar 10 s faz factory reset.
- Áudio por I²S (microfone + alto-falante com DAC
es8311); componentes externos do M5Stack (lp5562para o LED/retroiluminação ees8311). - Dá para escolher o motor de wake word (no dispositivo ou no HA) e a sensibilidade, além de um timer com som ao terminar.
Ative o Assist no Home Assistant, valide no ESPHome Device Builder e teste as palavras de ativação.
Exemplo pronto: leitor RFID/NFC com tela (M5Stack AtomS3R + RFID 2)#
Um leitor de RFID/NFC com telinha, feito com o M5Stack AtomS3R + unidade RFID 2 (WS1850S). Ao aproximar uma tag, ele mostra o UID na tela, conta as leituras e envia a tag ao Home Assistant (tag_scanned + um evento), onde você dispara automações — abrir a porta, ativar um cenário, identificar quem chegou.
Troque pelos seus dados antes de gravar. A chave da API e o IP fixo são exemplos; gere/ajuste os seus. Para mostrar um nome amigável da tag na tela, crie no HA um helper
input_text.rfid_tag_nome(opcional). Requer ESPHome 2026.7+.
substitutions:
name: atom-s3r-rfid
friendly_name: "ATOM S3R RFID"
esphome:
name: ${name}
friendly_name: ${friendly_name}
min_version: 2026.7.0
platformio_options:
board_build.flash_mode: opi
on_boot:
priority: -100
then:
- light.turn_on:
id: lcd_backlight
brightness: 100%
- delay: 500ms
- lambda: |-
id(scan_total).publish_state(id(scan_count));
if (!id(last_uid).empty()) {
id(last_uid_text).publish_state(id(last_uid));
id(uid_length).publish_state(
(float)((id(last_uid).size() + 1) / 3)
);
}
- component.update: meu_display
esp32:
variant: esp32s3
flash_size: 8MB
cpu_frequency: 240MHz
framework:
type: esp-idf
sdkconfig_options:
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
psram:
mode: octal
speed: 80MHz
enable_ecc: true
logger:
level: DEBUG
hardware_uart: USB_SERIAL_JTAG
api:
encryption:
key: !secret api_encryption_key # gere a sua no ESPHome Builder
ota:
- platform: esphome
encryption:
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
manual_ip: # ajuste à sua rede ou remova para DHCP
static_ip: 192.168.1.61
gateway: 192.168.1.1
subnet: 255.255.255.0
dns1: 8.8.8.8
dns2: 8.8.4.4
power_save_mode: NONE
external_components:
- source: github://m5stack/esphome-yaml/components@main
components:
- lp5562
refresh: 1d
globals:
- id: status_leitura
type: bool
restore_value: no
initial_value: "false"
- id: last_uid
type: std::string
restore_value: yes
initial_value: '""'
- id: scan_count
type: int
restore_value: yes
initial_value: "0"
- id: last_scan_ts
type: time_t
restore_value: yes
initial_value: "0"
i2c:
# Barramento interno: backlight LP5562
- id: bsp_bus
sda:
number: GPIO45
ignore_strapping_warning: true
scl:
number: GPIO0
ignore_strapping_warning: true
scan: true
# Port A: RFID 2 / WS1850S
- id: bus_grove
sda: GPIO2
scl: GPIO1
scan: true
spi:
id: spi_bus
clk_pin: GPIO15
mosi_pin: GPIO21
lp5562:
id: lp5562_led
i2c_id: bsp_bus
use_internal_clk: true
white_current: 17.5
output:
- platform: lp5562
id: lp5562_white_channel
lp5562_id: lp5562_led
channel: white
light:
- platform: monochromatic
name: "LCD Backlight"
id: lcd_backlight
output: lp5562_white_channel
icon: mdi:television
restore_mode: ALWAYS_ON
time:
- platform: sntp
id: sntp_time
servers:
- time.google.com
timezone: "America/Fortaleza"
rc522_i2c:
i2c_id: bus_grove
address: 0x28
update_interval: 0.5s
on_tag:
then:
- lambda: |-
id(last_uid) = x;
id(scan_count) += 1;
id(status_leitura) = true;
auto now = id(sntp_time).now();
if (now.is_valid()) {
id(last_scan_ts) = now.timestamp;
char buf[32];
now.strftime(
buf,
sizeof(buf),
"%d/%m/%Y %H:%M:%S"
);
id(last_scan_text).publish_state(buf);
}
id(last_uid_text).publish_state(x);
id(uid_length).publish_state(
(float)((x.size() + 1) / 3)
);
id(scan_total).publish_state(id(scan_count));
id(tag_present_bs).publish_state(true);
- homeassistant.tag_scanned: !lambda "return x;"
- homeassistant.event:
event: esphome.rfid_tag_scanned
data:
device: ${name}
data_template:
uid: "{{ uid }}"
variables:
uid: !lambda "return x;"
- component.update: meu_display
- delay: 3s
- lambda: |-
id(status_leitura) = false;
id(tag_present_bs).publish_state(false);
- component.update: meu_display
on_tag_removed:
then:
- lambda: |-
id(tag_present_bs).publish_state(false);
font:
- file: "gfonts://Roboto@700"
id: fonte_hora
size: 28
glyphsets:
- GF_Latin_Core
- file: "gfonts://Roboto"
id: fonte_texto
size: 16
glyphsets:
- GF_Latin_Core
- file: "gfonts://Roboto"
id: fonte_mini
size: 12
glyphsets:
- GF_Latin_Core
display:
- platform: mipi_spi
id: meu_display
spi_id: spi_bus
model: M5STACK-ATOMS3R-GC9107
rotation: 180°
update_interval: 1s
lambda: |-
it.fill(Color::BLACK);
Color white = Color(255, 255, 255);
Color gray = Color(150, 150, 150);
Color green = Color(0, 255, 0);
Color yellow = Color(255, 255, 0);
auto now = id(sntp_time).now();
if (now.is_valid()) {
it.strftime(64, 2, id(fonte_hora), white, TextAlign::TOP_CENTER, "%H:%M", now);
it.strftime(64, 38, id(fonte_texto), white, TextAlign::TOP_CENTER, "%d/%m/%Y", now);
} else {
it.print(64, 2, id(fonte_hora), white, TextAlign::TOP_CENTER, "--:--");
}
if (id(status_leitura)) {
it.print(64, 70, id(fonte_hora), yellow, TextAlign::CENTER, "READING!");
} else {
it.print(64, 70, id(fonte_hora), green, TextAlign::CENTER, "READY!");
}
std::string label = id(ha_tag_name).state;
if (label.empty() || label == "unknown" || label == "unavailable") {
label = id(last_uid);
}
if (!label.empty()) {
if (label.size() > 20) {
label = label.substr(0, 19) + "...";
}
it.printf(64, 110, id(fonte_mini), gray, TextAlign::CENTER, "%s", label.c_str());
}
text_sensor:
- platform: template
name: "Última TAG (UID)"
id: last_uid_text
icon: mdi:nfc-variant
update_interval: never
- platform: template
name: "Última leitura (data/hora)"
id: last_scan_text
icon: mdi:clock-outline
update_interval: never
- platform: homeassistant
id: ha_tag_name
entity_id: input_text.rfid_tag_nome
internal: true
- platform: wifi_info
ip_address:
name: "IP"
dns_address:
name: "DNS"
mac_address:
name: "MAC"
- platform: version
name: "Versão ESPHome"
entity_category: diagnostic
sensor:
- platform: template
name: "Total de leituras"
id: scan_total
icon: mdi:counter
accuracy_decimals: 0
state_class: total_increasing
update_interval: never
- platform: template
name: "Tamanho do UID (bytes)"
id: uid_length
icon: mdi:ruler
accuracy_decimals: 0
update_interval: never
- platform: template
name: "Tempo desde última leitura"
id: time_since_scan
icon: mdi:timer-sand
unit_of_measurement: s
device_class: duration
accuracy_decimals: 0
update_interval: 10s
lambda: |-
if (id(last_scan_ts) == 0) {
return NAN;
}
auto now = id(sntp_time).now();
if (!now.is_valid()) {
return NAN;
}
return (float)(now.timestamp - id(last_scan_ts));
- platform: wifi_signal
name: "Sinal WiFi"
update_interval: 60s
entity_category: diagnostic
- platform: uptime
name: "Uptime"
entity_category: diagnostic
binary_sensor:
- platform: template
name: "TAG presente"
id: tag_present_bs
device_class: presence
- platform: status
name: "Status"
entity_category: diagnostic
button:
- platform: template
name: "RFID: Zerar contador"
icon: mdi:counter
entity_category: config
on_press:
- lambda: |-
id(scan_count) = 0;
id(scan_total).publish_state(0);
- platform: template
name: "RFID: Limpar última TAG"
icon: mdi:nfc-variant-off
entity_category: config
on_press:
- lambda: |-
id(last_uid) = "";
id(last_scan_ts) = 0;
id(last_uid_text).publish_state("");
id(last_scan_text).publish_state("");
- component.update: meu_display
- platform: restart
name: "RFID: Reiniciar"
entity_category: config
web_server:
port: 80
include_internal: true
Como funciona, em resumo:
- O
rc522_i2clê as tags no Port A (Grove I²C). Noon_tag, o UID é guardado, o contador incrementa e a tag é enviada ao HA comhomeassistant.tag_scannede um evento (esphome.rfid_tag_scanned). - A tela (
mipi_spi, AtomS3R GC9107) mostra hora e data e o estado — READY! (ocioso) ou READING! (lendo) — além do nome/UID da última tag. - Sensores expõem ao HA: UID, data da última leitura, total de leituras, tamanho do UID, tempo desde a última leitura e presença de tag.
- Botões de configuração: zerar o contador, limpar a última tag e reiniciar.
No Home Assistant, use o gatilho “Tag” (ou o evento) para criar automações. Valide no ESPHome Device Builder antes de gravar.
Fontes: Bluetooth Proxy, remote_receiver (IR), OpenThread, LVGL, display ST7789V, http_request, touchscreen GT911, voice_assistant, micro_wake_word, RC522 (RFID) e Nabu Casa e Open Home Foundation.