# Como um controle de ventilador virou um clonador RF completo com ESP32, ESPHome e Home Assistant

> Começou como um hack para substituir uma bridge Tuya e clonar o controle do meu ventilador. Virou o esphome-rf-cloner, um clonador RF open source com backup, restore e integração nativa com o Home Assistant.

- Por: Antônio Miranda (https://antmiranda.dev)
- Publicado em: 23 de setembro de 2026
- URL: https://antmiranda.dev/pt-br/blog/in-depht/building-esphome-rf-cloner
- Tags: esphome-rf-cloner, esp32, esphome, home-assistant, rf, hardware, iot, projetos

Tudo isso começou porque um controle RF que eu já usava resolveu parar de funcionar do jeito que eu precisava.

Eu tenho um ventilador de teto Ventisol controlado por um controle remoto de 433 MHz. Durante um tempo eu usava uma daquelas bridges Tuya que misturam RF e infravermelho. Ela fazia exatamente o que eu queria: eu apertava um botão no controle, ela aprendia o sinal e depois eu conseguia reproduzir aquilo pelo Home Assistant.

Até deixar de ser uma solução em que eu queria confiar.

Comecei procurando uma substituta pronta. Minha ideia inicial era simples: encontrar algum dispositivo Zigbee ou Wi-Fi que fizesse aprendizado RF da mesma forma, parear no Home Assistant e seguir a vida.

Na prática, essa busca foi bem pior do que eu esperava.

Ou os dispositivos eram dependentes de cloud, ou a integração era limitada, ou custavam caro demais no Brasil, ou simplesmente não havia garantia nenhuma de que conseguiriam clonar aquele controle específico.

Depois de algum tempo tentando achar um produto pronto, cheguei na pergunta óbvia:

> Se o que eu preciso é receber um sinal RF, salvar e transmitir depois, por que não fazer eu mesmo?

E foi daí que nasceu o `esphome-rf-cloner`.

Hoje ele está no GitHub:

[github.com/codebyant/esphome-rf-cloner](https://github.com/codebyant/esphome-rf-cloner)

Mas o projeto que existe hoje é bem diferente daquele primeiro protótipo.

## O problema que eu queria resolver

Meu caso inicial era um ventilador de teto Ventisol, usando o controle modelo 13930.

O objetivo original era bem modesto:

1. apertar um botão no controle;
2. capturar o sinal RF;
3. salvar;
4. reproduzir usando um ESP32;
5. controlar pelo Home Assistant.

Eu não queria entender o protocolo do ventilador.

Não queria descobrir o significado de cada bit.

Não queria implementar um driver específico para Ventisol.

Eu só queria clonar o que o controle já fazia.

Esse detalhe acabou definindo boa parte do projeto depois.

## O hardware

Eu já tinha Home Assistant rodando em casa e boa parte da minha automação já estava integrada nele. Então a ideia de usar um ESP32 como ponte fazia bastante sentido.

Para o primeiro protótipo usei:

* ESP32 DevKit V1
* módulo CC1101 433 MHz
* jumpers
* o próprio controle Ventisol
* Home Assistant
* ESPHome

Os componentes exatos que usei foram estes:

* ESP32 DevKit V1 que usei: [versão USB-C](https://meli.la/1aAJke5) ou [versão Micro-USB](https://meli.la/11m5DHG)
* [Módulo CC1101 433 MHz que usei](https://meli.la/1NQqv8n)

O meu módulo é um E07-M1101D baseado no CC1101.

A ligação que acabou ficando validada foi:

| CC1101    | ESP32  |
| --------- | ------ |
| GND       | GND    |
| VCC       | 3V3    |
| GDO0      | GPIO22 |
| CSN       | GPIO5  |
| SCK       | GPIO18 |
| MOSI / SI | GPIO23 |
| MISO / SO | GPIO19 |
| GDO2      | GPIO4  |

Um ponto importante: o CC1101 trabalha em 3,3 V. Não liguei o módulo em 5 V.

{/* IMAGEM: foto do ESP32 + CC1101 montados (ainda não existe no repo) */}

## Antes de transmitir qualquer coisa, eu precisava descobrir o que estava no ar

No começo eu nem tinha certeza de que aquele controle estava realmente transmitindo exatamente onde eu imaginava.

A frequência nominal era 433,92 MHz, mas eu queria confirmar isso na prática.

Fiz primeiro uma varredura mais ampla com o CC1101. O teste passou por uma faixa de aproximadamente 387 a 464 MHz procurando atividade.

Foi uma das partes mais divertidas do projeto porque ali deixou de ser teoria. Eu apertava o controle e conseguia literalmente observar picos de atividade RF.

Depois fui estreitando até chegar na região correta.

O controle estava mesmo em 433,92 MHz.

A modulação era compatível com ASK/OOK.

A partir daí dava para parar de procurar pelo sinal e começar a olhar para o formato dele.

## Capturando o sinal bruto

Minha primeira abordagem foi deliberadamente simples.

Em vez de tentar interpretar o protocolo, comecei medindo o tempo entre as mudanças de nível do sinal recebido pelo CC1101.

O resultado era basicamente uma lista de pulsos positivos e negativos, em microssegundos.

Nas capturas do Ventisol apareciam tempos próximos de:

* pulso curto: aproximadamente 220 a 280 µs
* pulso longo: aproximadamente 700 a 760 µs

Uma captura típica tinha cerca de 65 timings.

O intervalo entre frames ficava por volta de 8,6 a 9,4 ms, dependendo da captura.

Em um dos primeiros testes estáveis, o frame equivalente que eu estava observando podia ser representado assim:

```text
00000001111110011000100000111110
```

Mas esse bitstream nunca virou o centro do projeto.

O que realmente importava era isto:

```text
HIGH  ~280 us
LOW   ~710 us
HIGH  ~280 us
LOW   ~710 us
...
gap   ~8660 us
```

Se eu conseguia reproduzir os mesmos tempos, talvez nem precisasse entender o protocolo.

## A primeira transmissão não funcionou

Capturar é uma coisa.

Fazer o aparelho aceitar o sinal transmitido é outra.

A primeira tentativa de replay não fez absolutamente nada no ventilador.

Foi aí que comecei a olhar com mais cuidado para:

* quantidade de pulsos;
* polaridade;
* intervalo entre frames;
* repetição;
* início e fim da captura;
* timings artificiais introduzidos pelo receiver.

Uma coisa importante que apareceu nesse processo foi um timing extra que não fazia parte do sinal físico, mas era consequência do fim da captura.

Também precisei parar de tratar o intervalo entre frames como algo inferido pelo tempo entre callbacks.

O melhor dado era o próprio waveform.

Se o controle transmitia:

```text
frame
gap
frame
gap
frame
```

eu queria guardar esse `gap` como parte real daquilo que tinha sido medido.

Depois desses ajustes, fiz outro replay.

Dessa vez o ventilador respondeu.

Esse foi o momento em que o projeto realmente começou.

## Eu não precisava descobrir o protocolo

Até ali eu ainda pensava que talvez acabasse tendo que fazer engenharia reversa completa no controle.

Descobrir endereço, comando, checksum, estrutura do frame e todas essas coisas.

Só que o replay bruto funcionou perfeitamente.

Então parei.

Para o objetivo daquele projeto, decodificar o protocolo seria trabalho extra sem benefício real.

Se eu consigo armazenar:

```text
frequência
modulação
timings
gap
quantidade de repetições
```

e depois reproduzir aquilo de forma fiel, eu já tenho um clonador.

Isso também tornava o projeto muito mais interessante.

Ele deixava de ser:

> controlador para ventilador Ventisol

e passava a ser:

> clonador genérico de comandos RF compatíveis.

Essa mudança parece pequena, mas foi provavelmente a decisão mais importante do projeto inteiro.

## O primeiro protótipo virou um learner

A primeira versão funcional ainda era bem manual.

Capturava um comando, imprimia timings, eu colocava aquilo no código e transmitia novamente.

Funcionava, mas era um brinquedo de bancada.

Eu queria o comportamento das bridges comerciais:

1. mandar aprender;
2. apertar o botão do controle;
3. dar um nome;
4. guardar;
5. nunca mais precisar recompilar firmware.

Então implementei um learner de verdade.

O ESP passou a ter um estado de aprendizado.

Quando armado, ele abre uma janela de aproximadamente 15 segundos aguardando RF.

O receiver usa um filtro de pulsos muito curtos, no meu caso 100 µs, e deixa uma janela suficientemente grande para observar repetições completas do frame.

O controle remoto normalmente transmite o mesmo frame várias vezes enquanto o botão fica pressionado.

Isso é ótimo para validação.

Em vez de confiar cegamente na primeira sequência recebida, eu podia comparar repetições e verificar se estava realmente capturando algo consistente.

{/* IMAGEM: logs ou captura do learner (ainda não existe no repo) */}

## O primeiro comando aprendido de verdade

Depois que essa parte ficou pronta, eu parei de alimentar o sistema com frames conhecidos.

Armei o learner.

Segurei um botão do controle.

O ESP capturou.

Salvou.

Depois mandei transmitir o que ele mesmo tinha aprendido.

O ventilador respondeu.

Nesse ponto eu já não estava mais testando um transmissor RF.

Eu tinha um clonador.

## Armazenamento persistente

O próximo problema era óbvio.

Se eu reiniciar o ESP e perder tudo, isso não serve como automação residencial.

Então os comandos precisavam sobreviver a reboot e reflash.

Criei um armazenamento próprio no dispositivo.

Cada comando passou a ter, entre outras coisas:

```text
command_id
name
frequency_hz
modulation
timings
gap_us
repeat_times
```

Uma decisão importante foi separar o nome da identidade.

O nome pode mudar.

O ID não.

Então um comando pode começar assim:

```text
command_id: 2
name: light
```

e depois virar:

```text
command_id: 2
name: luz_quarto
```

sem deixar de ser o mesmo comando.

Isso ficou ainda mais importante quando comecei a integrar com Home Assistant.

## IDs que nunca são reutilizados

Também decidi que `command_id` não deveria voltar a ser usado depois que um comando fosse apagado.

Se eu tivesse:

```text
1
2
3
```

apagasse o `3` e criasse outro comando, ele não deveria se tornar o novo `3`.

Viraria:

```text
4
```

Esse comportamento simplifica bastante a identidade fora do ESP.

Se Home Assistant conhece `command_id = 3`, aquele ID nunca vai representar outra coisa no futuro.

É uma regra pequena que evita um monte de problemas estranhos depois.

## O bridge também precisava de identidade

O mesmo raciocínio se aplicou ao próprio dispositivo.

Criei um `bridge_id` persistente, independente de IP, hostname ou do ESPHome.

No meu caso ele é um identificador de 16 bytes representado em hexadecimal.

Isso significa que a identidade lógica do bridge não depende do hardware físico estar sempre igual.

Essa decisão acabou permitindo uma funcionalidade que eu nem estava pensando no começo: substituição completa do ESP32 sem perder o relacionamento com Home Assistant.

## Revision

Também adicionei uma revision do registry.

Ela funciona como um número monotônico que representa mudanças reais no conjunto de comandos.

Transmitir um comando não altera revision.

Renomear, aprender ou apagar altera.

Com isso, se Home Assistant tem uma cópia do estado da revision 5 e o ESP continua em 5, não existe motivo para reconstruir tudo novamente.

Também tive que pensar no limite do número.

Como esse valor atravessa a API do ESPHome como inteiro assinado de 32 bits, defini explicitamente a faixa válida e reservei o valor terminal.

Nada de overflow silencioso.

Provavelmente isso nunca seria atingido numa instalação real, mas esse é exatamente o tipo de bug que prefiro eliminar quando o formato ainda está sob meu controle.

## Backup portátil

Nesse ponto já existia informação demais no ESP para eu aceitar que o único backup fosse a flash daquele microcontrolador.

Criei então um formato de exportação independente do formato físico usado no storage.

Um comando exportado se parece conceitualmente com isto:

```json
{
  "schema": 1,
  "found": true,
  "bridge_id": "...",
  "command_id": 2,
  "name": "light",
  "pulses": 65,
  "gap_us": 8669,
  "repeat_times": 20,
  "frequency_hz": 433920000,
  "modulation": 0,
  "timings": [
    280,
    -710
  ]
}
```

Os timings reais obviamente têm muito mais valores.

O ponto é que esse arquivo não depende do layout interno da flash.

Eu consigo mudar a implementação do storage no futuro sem invalidar necessariamente o formato de backup.

## Restore e troca de hardware

Depois do export, o restore veio quase naturalmente.

O protocolo que implementei ficou dividido em etapas:

```text
restore_begin

restore_commit
```

Enquanto um restore está aberto, operações normais ficam bloqueadas.

Se faltar energia no meio, o sistema consegue detectar que existe um restore incompleto.

Eu não queria um estado em que metade dos comandos viesse do backup e o ESP simplesmente fingisse que estava tudo certo.

### O teste que eu realmente queria fazer

Eu fiz um teste completo de substituição.

Primeiro tinha um bridge funcionando normalmente.

Fiz backup.

Depois fiz factory reset para o dispositivo nascer com outra identidade.

Home Assistant detectou que aquele hardware não era mais o bridge lógico que ele conhecia.

Em vez de adotar silenciosamente a identidade nova, o sistema congelou a reconciliação.

Depois fiz o restore.

O bridge antigo voltou a existir logicamente, com os mesmos IDs dos comandos.

O waveform exportado voltou bit por bit.

E, mais importante, depois disso eu mandei transmitir novamente.

O ventilador respondeu.

Não era só um JSON bonito.

Era backup de verdade.

## A integração com ESPHome

Desde cedo eu queria que isso funcionasse com ESPHome porque é como eu já gosto de expor dispositivos para Home Assistant.

Mas eu não queria que cada comando aprendido exigisse alterar YAML.

Esse era um requisito desde o início.

Aprender um novo botão do controle precisava ser uma operação de runtime.

Então o component expõe ações para coisas como:

```text
status
list
learn
send
send_id
delete
delete_id
rename
clear
cancel
factory_reset
export
restore_begin
import
restore_commit
```

A interface interna foi mudando bastante durante o projeto, mas a ideia permaneceu essa.

O ESP é a autoridade sobre os comandos RF.

Home Assistant pode organizar, exibir, fazer backup e restaurar.

Ele não é um segundo master daquele registry.

## Home Assistant sem uma segunda conexão ESPHome

Quando comecei a integração nativa com Home Assistant, uma das primeiras tentações seria abrir uma conexão própria usando a API do ESPHome.

Eu não fiz isso.

Home Assistant já tem uma integração ESPHome mantendo aquela conexão.

![O node RF Bridge na integração ESPHome do Home Assistant](https://antmiranda.dev/blog/esphome-rf-cloner/01-esphome-node.png)

Então a integração RF Cloner usa a própria camada de services/actions que o ESPHome já disponibiliza dentro do HA.

Isso evita duas conexões concorrentes com o mesmo dispositivo e mantém o RF Cloner acoplado ao config entry do ESPHome, não a IP fixo.

Essa decisão foi validada de um jeito quase engraçado durante o desenvolvimento.

O DHCP mudou o IP do ESP.

Nada quebrou.

Era exatamente isso que eu queria.

## A primeira versão do Home Assistant

Na primeira integração, cada comando aprendido virava uma entidade `button`.

Algo como:

```text
button.light
button.speed_1
button.power
```

O `unique_id` usa:

```text
{bridge_id}_{command_id}
```

Então renomear um comando não recria a entidade.

Mover coisas também não deveria recriar.

O botão continua sendo o mesmo botão para o Home Assistant.

Isso preserva:

* entity_id
* histórico
* automações
* dashboards
* customizações

Na versão inicial eu tomei uma decisão deliberada: comando não era Device.

Um botão do controle remoto não é um dispositivo físico.

Criar um Device por comando deixaria o Device Registry cheio de coisas sem significado.

## A v0.1.0

Quando esse primeiro ciclo ficou sólido, publiquei a `v0.1.0`.

Antes disso, fiz uma quantidade de testes que eu nunca imaginaria fazer quando liguei aquele primeiro CC1101 na bancada.

A suíte do firmware chegou a:

```text
579 checks
0 failures
```

Também existiam testes da lógica de integração e testes de restore.

Além disso eu fazia testes live no Home Assistant real porque algumas coisas simplesmente não valem muito quando simuladas.

Por exemplo:

* reiniciar HA;
* reiniciar ESP;
* renomear comando;
* apagar;
* restaurar;
* mudar hardware;
* transmitir RF de verdade.

Sempre que possível eu terminava o teste apertando um botão virtual e olhando para o aparelho físico.

Isso evita a situação clássica em que todos os mocks ficam verdes enquanto o ventilador continua parado.

## A v0.2 começou com uma pergunta simples

Depois da v0.1 surgiu um problema de organização.

Eu tinha comandos.

Mas comandos de quê?

Se eu tiver:

```text
light
speed_1
speed_2
power
```

eles pertencem ao ventilador.

Se eu também tiver:

```text
open
close
stop
```

esses podem pertencer ao portão.

No Home Assistant fazia muito mais sentido representar:

Foi daí que nasceu o conceito de RF Target, que na interface virou RF Device.

O ESP não sabe que existe um ventilador.

Isso é proposital.

Para o firmware continuam existindo apenas comandos RF.

A organização em equipamentos é responsabilidade do Home Assistant.

## RF Device é Device, comando é entidade

Essa separação ficou:

Na prática, a lista de entidades do Home Assistant fica agrupada pelo equipamento:

![Os comandos do ventilador agrupados sob o RF Device na lista de entidades](https://antmiranda.dev/blog/esphome-rf-cloner/08-command-entities.png)

Um RF Device tem:

* `target_id` próprio
* nome
* tipo
* área

Um comando continua tendo o mesmo `command_id`.

Mover `light` de um equipamento para outro não altera absolutamente nada no ESP.

Foi uma regra importante da v0.2.

Organização do Home Assistant não deve causar escrita desnecessária no registry RF.

## Ícones

Também adicionei ícones por comando.

Isso parece cosmético, mas faz muita diferença quando a lista começa a crescer.

O RF Cloner pode sugerir algo como:

```text
light -> mdi:lightbulb
power -> mdi:power
```

![Editando um comando: nome, RF Device e ícone](https://antmiranda.dev/blog/esphome-rf-cloner/12-edit-command.png)

Mas se o usuário configurar manualmente um ícone no Entity Registry do próprio Home Assistant, o override do usuário ganha.

Isso foi testado explicitamente.

O RF Cloner pode trocar o ícone padrão por baixo e o Home Assistant continua mostrando o escolhido manualmente.

Se o override for removido, o ícone mais recente da integração volta a aparecer.

## A migração da v0.1 para v0.2

Aqui o projeto ficou mais sério para mim.

A v0.1 já estava pública.

Eu não podia mais simplesmente decidir que o formato antigo era inconveniente e apagar tudo.

A migração precisava preservar instalações reais.

Os comandos da v0.1 já podiam ter:

* entity_id customizado;
* automações;
* histórico;
* área;
* ícone;
* nome alterado pelo usuário.

Então o teste de migração não era:

> os comandos apareceram depois?

Era:

> são exatamente as mesmas entidades?

No meu Home Assistant real validei:

```text
command_id
unique_id
entity_id
registry record id
```

antes e depois.

Tudo permaneceu igual.

E a migration não escreveu nada no ESP.

A revision continuou exatamente a mesma.

## Testar o que o framework realmente faz

Uma das coisas que esse projeto reforçou para mim foi o quanto testes contra a implementação real do Home Assistant importam.

Em um momento descobri que remover uma Config Subentry faz o HA remover os Devices e Entities pertencentes àquela subentry antes dos listeners da integração conseguirem fazer qualquer coisa.

Isso influenciava diretamente o comportamento de apagar um RF Device.

Minha regra era:

> apagar Ventilador não pode apagar os comandos RF.

Só deve remover a organização.

Então testei o comportamento de recuperação usando o próprio Deleted Entity Registry do Home Assistant.

Ao apagar o target:

```text
Ventilador
```

o comando:

```text
button.light
```

volta para Unassigned.

Mas continua com o mesmo:

```text
unique_id
entity_id
registry record
icon
customizações
```

Eu não quis acreditar nisso apenas pelo código.

Testei ao vivo.

Funcionou.

## Um bug que meus testes tinham aprovado

Um dos bugs mais interessantes apareceu com Area.

O RF Device tinha sido criado em:

```text
Quarto Marcus
```

Depois eu movi manualmente o Device pelo Home Assistant para:

```text
Sala de estar
```

O RF Cloner corretamente não sobrescrevia essa escolha no reload.

Até aí ótimo.

Depois abri o formulário da integração para colocá-lo de volta no Quarto Marcus.

O formulário já mostrava Quarto Marcus porque esse era o valor antigo salvo na subentry.

Então eu selecionava Quarto Marcus, enviava e nada acontecia.

O Device continuava na Sala.

O pior é que existia um teste cobrindo exatamente aquilo.

E ele passava.

O teste estava errado pela mesma razão que a implementação.

Os dois comparavam a área escolhida com o valor armazenado.

Mas a verdade naquele momento era o Device Registry.

Corrigi o formulário para usar a área atual do Device.

Depois escrevi o teste novamente com o cenário real:

```text
criar em Quarto
mover manualmente para Sala
abrir formulário
form deve mostrar Sala
escolher Quarto
submit
Device deve voltar para Quarto
```

Esse teste provavelmente vale mais que vários testes unitários escritos diretamente a partir da implementação.

## Aprender diretamente dentro de um target

O próximo teste foi aprender um comando escolhendo o `Ventilador` como RF Device.

O fluxo ficou:

```text
Name
RF Device
Icon
```

![O formulário de aprender comando: nome, RF Device e ícone](https://antmiranda.dev/blog/esphome-rf-cloner/05-learn-command-form.png)

Armei o capture.

Segurei o controle.

![O Home Assistant esperando o botão do controle ser pressionado](https://antmiranda.dev/blog/esphome-rf-cloner/06-learn-command-waiting.png)

O ESP aprendeu.

O command recebeu um ID novo.

Home Assistant criou a entidade diretamente no Device Ventilador.

Sem entidade temporária em Unassigned.

![A página do RF Device do ventilador, com um botão por comando aprendido](https://antmiranda.dev/blog/esphome-rf-cloner/07-rf-device-page.png)

Depois mandei transmitir pelo botão recém-criado.

O ventilador respondeu.

Mais uma vez, o teste terminava no mundo físico.

## E depois eu deletei o comando

Para fechar o ciclo, apaguei só aquele comando descartável.

Eu acompanhei o event bus do Home Assistant para ter certeza de que a integração estava chamando:

```text
rf_delete_id
```

com o ID imutável.

Não a versão baseada em nome.

O command sumiu do ESP e do Home Assistant.

A revision avançou uma vez.

O ID foi aposentado.

O próximo ID não voltou atrás.

O snapshot foi atualizado.

Os outros comandos continuaram intactos.

## A v0.2.0

Depois desse ciclo publiquei a v0.2.0.

Nessa altura os testes estavam em:

```text
Firmware
579 checks
0 failures

Integration logic
24 checks
0 failures

Home Assistant
40 passed
```

Além disso passei por:

* compileall
* translation parity
* hassfest
* HACS validation
* CI no GitHub
* testes live com hardware

Tudo verde.

A tag da v0.2.0 foi publicada com o firmware e o formato de backup RF exatamente iguais aos da v0.1.

A v0.2 era essencialmente uma evolução da integração com Home Assistant.

## E obviamente encontrei um bug logo depois

Depois da release resolvi fazer uma coisa que parece ridiculamente óbvia em retrospecto.

Apaguei a configuração e tentei instalar como se eu nunca tivesse visto o projeto antes.

{/* IMAGEM: modal vazio "Connect an RF bridge" (não há print dele, só da versão corrigida) */}

A tela inicial apareceu assim:

Título.

Botão de enviar.

Nenhum campo.

O curioso é que o backend estava perfeito.

Ele retornava:

```text
ESPHome device
Action prefix
```

O problema estava no frontend do Home Assistant.

O selector usado era um `ConfigEntrySelector` obrigatório sem valor inicial.

No frontend 2026.9 isso passava por uma função que tentava produzir os valores iniciais do formulário e acabava lançando:

```text
Selector config_entry not supported in initial form data
```

A exceção interrompia o render.

Por isso sobravam apenas o título e o botão, que estavam fora da subtree quebrada.

Foi um bug particularmente bom de encontrar porque qualquer teste apenas no backend teria passado.

O JSON estava certo.

O Home Assistant estava serializando certo.

O problema só existia no browser.

A correção foi usar um selector normal preenchido com as config entries ESPHome disponíveis, mantendo como valor o `config_entry_id`.

Depois disso o onboarding finalmente passou a funcionar de verdade.

![O formulário Connect an RF bridge depois da correção, com os dois campos](https://antmiranda.dev/blog/esphome-rf-cloner/02-connect-bridge.png)

## Testando como usuário mostrou outro problema

Com o formulário consertado, continuei usando a integração como se fosse alguém instalando pela primeira vez.

E percebi outro problema.

Não técnico.

De UX.

Criar um RF Device era fácil.

A página da integração já tinha:

```text
Add an RF device
```

Criei:

```text
Ventilador
Fan
Quarto Marcus
```

Só que aprender um comando estava escondido na engrenagem da bridge.

O caminho era algo como:

```text
RF Cloner
Configuração da bridge
RF commands
Learn a command
```

O fluxo funcionava.

Mas eu mesmo fiquei procurando onde estava a ação principal do produto.

Então a próxima correção ficou óbvia.

Na tela principal da integração eu quero duas ações:

```text
Add an RF device
Learn command
```

Sem inventar dashboard customizado.

Sem criar frontend paralelo.

Sem criar entidade falsa de administração.

O Home Assistant já tem quase todas as peças.

O problema era só tornar a operação principal descobrível.

Esse era exatamente o estágio do projeto quando comecei a escrever este post. Antes de terminar o texto, o botão já estava lá, no main do repositório, esperando a próxima release:

![A página da integração com Add an RF device e Learn command lado a lado](https://antmiranda.dev/blog/esphome-rf-cloner/03-integration-page.png)

## O que o projeto virou

É engraçado olhar para trás porque eu só queria substituir uma bridge Tuya.

Hoje o projeto tem uma divisão bem clara.

### No ESP

O ESP é responsável pelo que é RF de verdade:

* aprender;
* validar;
* armazenar;
* transmitir;
* apagar;
* renomear;
* exportar;
* restaurar;
* manter IDs;
* manter revision.

![A página do bridge no Home Assistant: comandos armazenados, estado do aprendizado e diagnósticos do storage](https://antmiranda.dev/blog/esphome-rf-cloner/09-bridge-device-page.png)

### No Home Assistant

Home Assistant cuida da organização e da experiência:

* RF Devices;
* commands como entities;
* Areas;
* icons;
* automations;
* snapshots;
* replacement;
* migration;
* interface.

O firmware não sabe que aquele sinal liga a luz de um ventilador.

Ele sabe que existe um `command_id 4` com um waveform. Home Assistant sabe que aquele mesmo `command_id 4` é a Luz do Ventilador:

Eu gosto bastante dessa separação.

## O que eu faria diferente se começasse hoje

Provavelmente quase nada na primeira parte.

Eu ainda começaria pelo replay bruto.

Ainda evitaria engenharia reversa do protocolo sem necessidade.

Ainda validaria o RF fisicamente antes de pensar em Home Assistant.

O que eu faria mais cedo seria testar o produto como usuário novo.

É muito fácil desenvolver uma integração sabendo exatamente:

* qual botão clicar;
* onde está o flow;
* qual campo significa o quê;
* qual estado interno deveria aparecer.

Quando você apaga tudo e tenta instalar de novo sem usar atalhos, encontra uma categoria completamente diferente de bugs.

O modal vazio foi um deles.

A ação de Learn escondida foi outro.

## Sobre usar IA nesse projeto

Boa parte da implementação e da investigação foi feita com agentes de código, principalmente para acelerar coisas que seriam extremamente repetitivas de fazer manualmente.

Mas eu não tratei isso como:

> descrevo o projeto e espero sair pronto.

Meu processo foi muito mais próximo de:

```text
hipótese
implementação
hardware
medição
falha
logs
nova hipótese
teste
regressão
teste real
```

Várias decisões que pareciam corretas foram descartadas depois de testar no hardware ou no Home Assistant real.

Também teve teste que estava verde e estava provando a coisa errada.

IA acelera bastante esse tipo de projeto, mas hardware continua tendo uma habilidade excelente de destruir qualquer teoria bonita.

Se o ventilador não gira, o teste falhou.

## O resultado

Hoje eu tenho um ESP32 com CC1101 que consegue aprender comandos RF em runtime, salvar, sobreviver a reboot, exportar, restaurar e expor tudo para Home Assistant sem precisar recompilar a cada botão novo.

E o sistema não ficou preso ao Ventisol.

Esse foi só o primeiro aparelho que usei para provar que a ideia funcionava.

Qualquer controle compatível com a forma de captura e transmissão pode potencialmente ser aprendido da mesma forma.

Eu também deixei intencionalmente algumas coisas fora do escopo.

Não existe tentativa de:

* decodificar todos os protocolos RF;
* suportar rolling code;
* inferir estado do aparelho;
* fingir que um botão RF é uma entidade `fan` stateful sem feedback;
* mandar dados para cloud;
* adicionar IR só porque módulos comerciais normalmente misturam os dois.

O projeto faz uma coisa.

Aprende RF bruto e reproduz com fidelidade.

O restante é organização.

## Montando o seu

Se você quiser repetir exatamente o meu setup, os dois componentes principais são estes:

* ESP32 DevKit V1: [USB-C](https://meli.la/1aAJke5) ou [Micro-USB](https://meli.la/11m5DHG)
* [CC1101 / E07-M1101D 433 MHz](https://meli.la/1NQqv8n)

Ligação:

| CC1101    | ESP32  |
| --------- | ------ |
| GND       | GND    |
| VCC       | 3V3    |
| GDO0      | GPIO22 |
| CSN       | GPIO5  |
| SCK       | GPIO18 |
| MOSI / SI | GPIO23 |
| MISO / SO | GPIO19 |
| GDO2      | GPIO4  |

O projeto, configurações e documentação estão aqui:

[github.com/codebyant/esphome-rf-cloner](https://github.com/codebyant/esphome-rf-cloner)

A documentação do repositório é a referência para instalação porque a interface ainda está evoluindo e eu prefiro não duplicar instruções que podem ficar desatualizadas neste post.

## Algumas observações antes de comprar qualquer coisa

Nem todo RF de 433 MHz é simplesmente clonável.

Existem controles com rolling code, protocolos diferentes, frequências diferentes e características que podem exigir outra abordagem.

Portão de garagem é um exemplo clássico em que eu não assumiria que replay bruto vai funcionar.

Também vale conferir a frequência do seu controle antes de comprar um módulo específico.

O meu caso era 433,92 MHz e foi nele que validei todo o projeto.

Não trate este projeto como uma forma universal de burlar sistemas RF.

A ideia é automatizar dispositivos que você já controla e que usam sinais compatíveis com replay.

## O próximo passo

A parte RF chegou num ponto em que eu considero muito sólida.

O que mais estou refinando agora é a experiência dentro do Home Assistant.

Quero que instalar a integração e ensinar o primeiro botão seja óbvio sem precisar ler a implementação ou caçar opções escondidas.

Esse é provavelmente o melhor sinal de que o projeto deixou de ser só aquele hack inicial.

No começo eu estava satisfeito quando o ventilador girava.

Agora eu fico incomodado quando o botão de aprender comando não está no lugar mais óbvio possível.

Acho que é assim que um projeto pessoal começa a virar um produto de verdade.
