Estação Termo: estação ambiental redonda com ESP32, BME280 e display GC9A01

A Estação Termo mede o ambiente da casa num só sítio: temperatura, humidade relativa e absoluta, ponto de orvalho, pressão e altitude, calculadas a partir de um único BME280 e apresentadas num display redondo de 1,28 polegadas. Um ESP32 com ESPHome desenha tudo, sincroniza a hora com o Home Assistant e apaga o ecrã quando ninguém está a ver. Tudo local, sem nuvem e sem subscrições.
Neste artigo
Visão geral do sistema
A ideia é ter uma estação meteorológica de interior que responda a duas perguntas: como está o ar da casa agora, e o que é preciso fazer a respeito disso. Um único BME280 dá temperatura, pressão e humidade relativa; o YAML faz o resto — humidade absoluta em g/m³, ponto de orvalho e altitude calculadas a partir desses três valores, sem sensores adicionais.
O que distingue o projeto não é o sensor, é a forma como apresenta os dados: um display redondo de 1,28 polegadas onde o fundo é uma imagem desenhada à mão (setores, ícones, zonas) e o ESPHome escreve apenas os números por cima. O resultado lê-se de relance — temperatura em destaque no topo, hora por baixo, humidade à esquerda, g/m³ e ponto de orvalho à direita, pressão e altitude na base — com o estado Online/Offline sempre visível.
- 240×240Display redondo GC9A01A de 1,28", SPI a 20 MHz
- 6Grandezas no ecrã: temperatura, humidade, orvalho, g/m³, pressão e altitude
- 60sIntervalo de leitura do BME280 (com offset de calibração)
- 0Serviços externos ou dependência da nuvem
O ecrã não está sempre ligado: uma tecla acorda a retroiluminação a 100%, e depois de 20 segundos o display volta a baixar e a apagar-se sozinho. A retroiluminação é também uma entidade light no Home Assistant, portanto dá para ligar, desligar e regular a luminosidade à distância — ou por automação, sem tocar em nada.
Hardware e esquema de ligação
A lista de materiais é curta — o projeto é barato porque é essencial:
- Placa ESP32 (
board: esp32dev) — sobram pinos para o SPI, o I²C, a tecla e a retroiluminação. - BME280 — temperatura, pressão e humidade relativa num único chip, por I²C. Cuidado: o BMP280 é quase igual e não mede humidade — não confundir as siglas na compra.
- Display redondo TENSTAR ROBOT de 1,28" — 240×240 pixels, controlador GC9A01A, interface SPI até 20 MHz, com retroiluminação própria.
- Tecla de contacto com pull-up externo de 10 kΩ — o GPIO usado é de entrada pura e não tem resistências internas.
- Dois ficheiros de assets ao lado do YAML: a imagem de fundo do ecrã (
icons/imageround.png) e a fonteHelvetica.ttf. Sem eles, o ESPHome nem compila. - Fonte 5 V — o regulador 3,3 V da própria placa alimenta o display e o sensor; CAIXA com janela redonda ou suporte de bancada.

Três erros de bancada que custam uma tarde: alimentar o display a 5 V (os módulos GC9A01 queimam — usa o 3,3 V da placa); esquecer o pull-up da tecla no GPIO35, que é input-only e não tem resistência interna; e montar o BME280 coladinho ao ESP32, onde lê sempre 2–3 °C a mais.
Mapeamento dos GPIO
Nove pinos em uso, divididos por barramentos — SPI para o ecrã, I²C para o sensor e dois pinos avulsos para a retroiluminação e a tecla:
| Sinal | GPIO | Direção | Função |
|---|---|---|---|
| SPI MOSI | GPIO27 | Saída | Dados do display (sem MISO — é write-only) |
| SPI CLK | GPIO14 | Saída | Clock SPI a 20 MHz |
| SPI CS | GPIO33 | Saída | Chip select do GC9A01A |
| SPI DC | GPIO25 | Saída | Distinção dados/comandos |
| SPI RST | GPIO26 | Saída | Reset do controlador |
| I²C SDA | GPIO21 | Bidirecional | BME280 (endereço 0x76) |
| I²C SCL | GPIO22 | Saída | Relógio do barramento |
| Backlight | GPIO32 | Saída PWM (LEDC) | Retroiluminação — entidade light |
| Tecla | GPIO35 | Entrada (inverted) | Acorda e apaga o ecrã |
Não há fio MISO no SPI porque o display nunca devolve dados: escreve e cala-se, o que também permite correr o barramento ao ritmo máximo sem seguranças. O I²C é o clássico SDA 21 / SCL 22 com scan: true, para que o endereço apareça no log na primeira arrancada.
O YAML, bloco a bloco
O estacao_termo.yaml separa-se bem: primeiro o ecrã, depois os sensores, depois as contas e por fim a retroiluminação. Estes são os excertos que interessam:
O display redondo: GC9A01A sobre SPI
spi:
mosi_pin: GPIO27
clk_pin: GPIO14
spi_device:
id: spidev
data_rate: 20MHz
display:
- platform: ili9xxx
model: gc9a01a
invert_colors: true
auto_clear_enabled: false
id: gca901_display
cs_pin: GPIO33
dc_pin: GPIO25
reset_pin: GPIO26
rotation: 180
lambda: |-
it.image(0, 0, id(img_back));
it.strftime(33, 65, id(helvetica_18), id(my_green), TextAlign::LEFT,
"%Y-%m-%d %H:%M:%S", id(esptime).now());
it.printf(97, 25, id(helvetica_24), "%.1f °C", id(bme280_temperature).state);
it.printf(46, 112, id(helvetica_24), "%.1f %%", id(bme280_humidity).state);
if (id(system_status).state) {
it.print(135, 1, id(helvetica_12), id(my_green), TextAlign::TOP_RIGHT, "Online");
} else {
it.print(135, 1, id(helvetica_12), id(my_red), TextAlign::TOP_RIGHT, "Offline");
}Code language: PHP (php)
Porquê auto_clear_enabled: false? Porque o fundo do ecrã é uma imagem que não muda: em vez de o ESPHome limpar o buffer a cada refresh e voltar a desenhar tudo (visível como um relampejo), mantém-se o buffer e escrevem-se apenas os números que mudam. Menos trabalho de CPU, ecrã mais estável — e é por isso que a lambda começa sempre com it.image(...).
As restantes linhas do ecrã seguem o mesmo padrão — pressão em (20, 160), altitude em (135, 160), humidade absoluta em (160, 105) e ponto de orvalho em (160, 130) — todas com it.printf e as coordenadas afinadas para assentar nas zonas do fundo.
O fundo é uma imagem, não código
image:
- file: "icons/imageround.png"
id: img_back
type: RGB
transparency: alpha_channel
font:
- file: "Helvetica.ttf"
id: helvetica_24
size: 24
# helvetica_18, helvetica_14 e helvetica_12 no mesmo padrão
color:
- id: my_green
red: 0%
green: 100%
blue: 0%
# my_red, my_yellow, my_blue e my_gray no mesmo padrãoCode language: PHP (php)
A imagem de fundo traz os setores, os ícones (termómetro, gota, montanha, bússola) e as molduras prontas — o ESPHome só recebe a tarefa de pôr o texto certo em cada zona. Fazer o desenho num editor e deixar ao firmware apenas os números é muito mais simples do que tentar construir a interface toda com it.line() e it.rectangle().

O ficheiro de imagem tem de ficar na pasta do projeto, em icons/imageround.png, e a fonte em Helvetica.ttf ao lado do YAML — o ESPHome copia ambos para a placa na compilação. Se mudares o fundo, mantém a mesma janela circular de 240×240 e refaz só as coordenadas do texto.
O BME280 e o que ele dá de borla
i2c:
sda: 21
scl: 22
scan: true
id: bus_a
sensor:
- platform: bme280_i2c
temperature:
name: "BME Temperatura"
oversampling: 1x
filters:
- offset: -2
id: bme280_temperature
pressure:
name: "BME Pressão"
oversampling: 1x
id: bme280_pressure
humidity:
name: "BME280 Humidade Relativa"
oversampling: 1x
id: bme280_humidity
address: 0x76
update_interval: 60sCode language: JavaScript (javascript)
Três valores por um chip só, com oversampling: 1x para leituras rápidas e um offset: −2 aplicado à temperatura — o suficiente para corrigir o calor que o próprio módulo devolve. O endereço 0x76 é o do módulo em questão; muitos BME280 vêm de fábrica em 0x77, por isso vale a pena deixar o scan: true ligado pelo menos na primeira arrancada.
Grandezas derivadas: altitude, g/m³ e ponto de orvalho
sensor:
- platform: template
name: "Altitude"
id: bme_altitude
lambda: |-
const float STANDARD_SEA_LEVEL_PRESSURE = 1013.25; // em hPa
return ((id(bme280_temperature).state + 273.15) / 0.0065) *
(powf((STANDARD_SEA_LEVEL_PRESSURE / id(bme280_pressure).state), 0.190234) - 1);
update_interval: 60s
unit_of_measurement: 'm'
icon: 'mdi:signal'
- platform: absolute_humidity
name: "Humidade Absoluta"
id: bme_humidadeabs
temperature: bme280_temperature
humidity: bme280_humidity
- platform: template
name: "Ponto Orvalho"
id: bme_orvalho
lambda: |-
return (243.5*(log(id(bme280_humidity).state/100)+((17.67*id(bme280_temperature).state)/
(243.5+id(bme280_temperature).state)))/(17.67-log(id(bme280_humidity).state/100)-
((17.67*id(bme280_temperature).state)/(243.5+id(bme280_temperature).state))));
unit_of_measurement: °C
icon: 'mdi:thermometer-alert'Code language: JavaScript (javascript)
Porque o ponto de orvalho? É a temperatura a que o ar satura e a água passa a condensar — janelas molhadas de manhã, espelhos embaçados, bolor nos cantos. Se a temperatura de uma superfície ficar abaixo do ponto de orvalho medido, aí é que a condensação acontece. É a grandeza que transforma um mostrador bonito numa ferramenta útil, e sai de graça das mesmas duas medições. O mesmo com a humidade absoluta em g/m³: é ela que diz quanto é que o ar realmente leva de água, independentemente da temperatura.
Retroiluminação e tecla
output:
- platform: ledc
pin: GPIO32
id: backlight
light:
- platform: monochromatic
output: backlight
name: "Backlight"
id: luzback
binary_sensor:
- platform: status
name: "Node Status"
id: system_status
- platform: gpio
pin:
number: GPIO35
inverted: true
name: "Tecla"
id: tdisplay_button_input_1
on_click:
then:
- light.turn_on:
id: luzback
brightness: 100%
transition_length: 2s
- delay: 20s
- light.turn_on:
id: luzback
brightness: 15%
transition_length: 60sCode language: JavaScript (javascript)
O truque é o LEDC: o mesmo GPIO que alimenta a retroiluminação é gerido como uma entidade light, o que dá três coisas de uma vez — brilho regulável no Home Assistant, transições suaves e uma sequência por automação. Aqui a sequência é simples: um clique acende a 100% com transição de 2 s e, 20 segundos depois, o ecrã começa a baixar e a apagar-se — sem ninguém precisar de se lembrar de o desligar.
Nota de implementação: no ficheiro original a sequência termina com um light.turn_off imediatamente a seguir ao fade de 60 s, o que o corta antes de terminar. Se quiseres que o ecrã afade até 0 ao longo de um minuto, remove o turn_off e deixa só o light.turn_on com brightness: 0% e transition_length: 60s.
Rede, gestão e atualizações
Como nos outros projetos deste blog, a estação vive inteiramente na rede local:
- IP estático —
192.168.1.238, com gateway e DNS em192.168.1.254. O endereço nunca muda, o que torna o dispositivo previsível para automações e para o web server. - Captive portal — configurável por telemóvel se o Wi-Fi não estiver presente (com a ressalva do ponto seguinte).
- Improv Serial — provisionamento de Wi-Fi pela porta série, a primeira vez ou depois de um reset.
- OTA — atualizações sem cabo, direto do ESPHome no Home Assistant.
- Web Server — página local com sensores e logs, útil para diagnóstico sem abrir o HA.
- API nativo — ligação direta ao Home Assistant, incluindo o relógio usado no ecrã.
- dashboard_import — o dispositivo pode ser importado como pacote no ESPHome Dashboard.
wifi:
ssid: SF_24E
password: "••••••••"
manual_ip:
static_ip: 192.168.1.238
gateway: 192.168.1.254
subnet: 255.255.255.0
dns1: 192.168.1.254
#ap: {}
captive_portal:
web_server:
ota:
- platform: esphomeCode language: CSS (css)
Duas notas de rede: o Access Point de emergência está comentado (#ap: {}) — sem ele, o captive_portal não tem por onde servir a página de configuração e, se trocares de router ou mudares a palavra-passe, só recuperas o dispositivo por USB ou pela porta série. E a palavra-passe do Wi-Fi está em texto no ficheiro: mantém o estacao_termo.yaml fora de repositórios públicos e define api: encryption_key e ota: password antes de o publicares.
Instalação passo a passo
Do ficheiro em branco à estação montada, em seis passos:
- Junta os assets. Cria a pasta
icons/ao lado doestacao_termo.yaml, mete lá oimageround.pnge copia oHelvetica.ttfpara a raiz do projeto. Sem estes dois ficheiros, a compilação falha antes de chegar ao resto. - Liga a fiação. Display por SPI (5 fios + 3,3 V e GND), BME280 por I²C (2 fios + 3,3 V e GND) e tecla entre o GPIO35 e o GND, com o pull-up de 10 kΩ para os 3,3 V.
- Grava e confirma o barramento. Adiciona o dispositivo no ESPHome, ajusta SSID, palavra-passe e IP e envia. No log, procura a linha do
i2c scan— tem de lá estar o0x76. - Aceita no Home Assistant. Surgem no minuto seguinte seis sensores, o light da retroiluminação, a tecla e o estado do nó.
- Calibra. Compara a temperatura com um termómetro de referência e ajusta o
offset; se a altitude der errada, troca os 1013,25 hPa do padrão pela pressão média do teu local. - Monta e testa. BME280 afastado do calor do ESP32, display à vista e tecla acessível. Clica: o ecrã acende, e sozinho apaga-se passados os 20 segundos.
No Home Assistant
A estação não é um gadget que só serve para olhar: cada valor que aparece no ecrã é uma entidade no Home Assistant, pronta para gráficos, automações e alertas.
Seis sensores, uma entidade de estado
BME Temperatura, BME Pressão, BME280 Humidade Relativa, Altitude, Humidade Absoluta e Ponto Orvalho — mais Wifi signal, Uptime e Node Status para diagnóstico. É o suficiente para montar um cartão de ambiente completo sem comprar mais nada.
A retroiluminação é uma luz
A entidade light.backlight aceita brilho como qualquer lâmpada inteligente. Dá para a ligar quando alguém entra na sala, a escurecer à noite ou a acender quando a humidade passa de um limite — sem escrever uma linha no ESPHome.
A hora no ecrã vem do HA
O time usa a plataforma homeassistant: o relógio do display é o mesmo do HA, sem NTP e sem desvios entre o que o ecrã mostra e o que as automações registam no logbook.
Estado visível e página web local
O Online/Offline no canto do ecrã é a entidade status, e o web server do ESPHome permite abrir 192.168.1.238 no browser e ver sensores e logs mesmo quando o Home Assistant está em manutenção.
Automações que valem a pena
- Alerta de condensação — se o Ponto Orvalho subir acima de uma temperatura de referência (paredes frias, vidros), notificação para arejar ou ligar o exaustor.
- Conforto de humidade — abaixo de 35% avisa para ar seco; acima de 65%, sugere ventilação antes de o bolor aparecer.
- Backlight automático — liga o ecrã à hora de acordar e apaga-o à noite, em vez de esperar pelo clique da tecla.
- Histórico do ar — exporta temperatura, humidade e orvalho para InfluxDB/Grafana e tens a curva do inverno inteiro, com a condensação a aparecer nos gráficos antes de aparecer nas janelas.
Diagnóstico e boas práticas
Se algo não bater certo, estes seis pontos cobrem os problemas reais deste projeto:
- O ecrã fica preto ou com as cores trocadas. Confirma os 3,3 V, os cinco fios SPI e os pinos
cs_pin,dc_pinereset_pin. Se aparecer a imagem mas invertida, mexe eminvert_colorserotation— o módulo montado de cabeça para baixo precisa dos 180°. Communication with BME280 failedno log. O endereço padrão do ESPHome é0x77e o do módulo pode ser0x76(ou vice-versa); lê a saída doscan: truee corrige. Verifica também as resistências de pull-up no SDA/SCL e, em modo I²C, que o pino CSB do chip está nos 3,3 V.- A temperatura lê sempre alta. É o clássico self-heating: o BME280 perto do ESP32 e da fonte. Afasta-o alguns centímetros, mede contra um termómetro de referência e afina o
offsetem vez de acreditar no número que sai da caixa. - A altitude dá 0 m. A fórmula divide pela pressão: se o sensor de pressão estiver em
NAN, o resultado cai a zero. Confirma a entidadeid(bme280_pressure)e, se for de propósito, ajusta oSTANDARD_SEA_LEVEL_PRESSUREà pressão média do teu local. - A tecla não acorda o ecrã. O
GPIO35é de entrada pura, sem pull-up interno: sem a resistência externa de 10 kΩ, o pino flutua e oinverted: truenunca dispara. - A compilação falha à procura de
icons/imageround.pngou deHelvetica.ttf. Os caminhos são relativos à pasta do projeto do ESPHome — confirma a estrutura antes de suspeitar do YAML.
O backup deste projeto são três coisas: o estacao_termo.yaml, o imageround.png e o Helvetica.ttf. Guarda os três juntos — recuperar a estação noutro ESP32 é só copiar a pasta e gravar.
Conclusão
A Estação Termo prova que um mostrador de ambiente não precisa de ser um ecrã retangular com números empilhados: com um display redondo de poucos euros, uma imagem de fundo bem desenhada e a lambda certa, obtém-se um painel que se lê de relance e que qualquer pessoa em casa sabe interpretar — temperatura, humidade, hora e basta.
Por baixo da capa está o que interessa: um BME280 a medir de 60 em 60 segundos, três grandezas calculadas no próprio firmware, retroiluminação que se apaga sozinha e todas as entidades no Home Assistant para as automações que forem precisas. É replicável ao fim de uma tarde — copia o estacao_termo.yaml, mete os dois assets na pasta certa e ajusta o offset ao teu caso. Tudo local, tudo teu.
Quer montar o teu?
A documentação do ESPHome cobre o controlador GC9A01, o sensor BME280 e o componente de imagem — e o YAML deste artigo serve de ponto de partida.
