Tomadas Tuya pelo Home Assistant, por código
Olá meus Unicórnios! 🦄✨
Eu tenho um projeto em casa que vigia o nobreak do escritório, mede o consumo das tomadas e, numa queda de energia, vai desligando as coisas em ordem para a bateria durar até o computador crítico poder ser salvo. Duas tomadas Tuya inteligentes fazem a medição — uma na carga crítica, outra no que pode morrer sem dó.
Até semana passada, o backend falava direto com a nuvem da Tuya: assinatura
HMAC-SHA256 em cada requisição, token com validade, um devices.json
para traduzir nome em deviceId e as escalas de cada medida. Funciona,
mas é bastante protocolo morando do meu lado.
Funcionava bem. Até o dia em que parou de funcionar — e é por isso que este artigo existe.
💥 O dia em que a nuvem da Tuya me abandonou
Três e pouco da manhã. Meu sistema simplesmente parou de enxergar as tomadas. Nada de leitura, nada de watts, nada. O painel inteiro cego. 😱
Minha primeira suspeita foi a de sempre: credencial. Chave errada, token expirado, Local Key trocada depois de reprovisionar. Fui no log procurar a mensagem — e o que estava lá não era nada disso:
{"code":28841004,
"erro":"Tuya API /v1.0/iot-03/devices/eb05...ann/status falhou:
IoT Core trial quota is exhausted. (code 28841004)",
"level":"error","message":"falha","tomada":"PcCritico"}
"IoT Core trial quota is exhausted." A cota do projeto na plataforma IoT da Tuya tinha acabado. 🫠
E aqui vai o detalhe cruel: a minha assinatura HMAC estava
perfeita. A nuvem aceitava meu client_id, validava a
assinatura, conferia o token — e só então respondia "sua cota acabou". Não era
bug meu, não era credencial, não era Local Key. Trocar as chaves no
.env não resolveria absolutamente nada, e eu perdi um tempo bom
tentando exatamente isso antes de ler o código de erro com atenção.
O tamanho do estrago, contado pelo log daquele dia:
$ grep -c "28841004" backend/logs/tuya.log
9750
$ grep -c '"leitura"' backend/logs/tuya.log
511
9.750 falhas contra 511 leituras boas em um único dia — mais de dez horas de sistema cego, da madrugada até o meio da tarde. E, o que me tirou o sono: se tivesse faltado energia naquela madrugada, a Auto Proteção não teria a menor ideia do que estava acontecendo com as tomadas. O nobreak seguiria descarregando enquanto meu painel mostrava... nada.
Descobri depois que a cota do IoT Core é um trial de mais ou menos
um mês, que você renova no iot.tuya.com → Cloud → projeto →
Service API. Renovei, voltou a funcionar. Mas o recado ficou:
meu sistema de proteção contra queda de energia dependia de uma cota
gratuita expirando do outro lado do mundo, sem me avisar. 😤
Foi esse susto que me fez subir uma VM com Home Assistant. Ela já enxergava
as duas tomadas, e a ideia foi imediata: e se eu falasse com elas pelo
HA? Some a assinatura, some o token da Tuya, some o
devices.json — e, principalmente, some aquela dependência que tinha
acabado de me deixar na mão.
Funcionou — e é sobre isso este artigo. Mas eu descobri, medindo, que duas das coisas que eu achava que ganharia simplesmente não eram verdade. 😅 A primeira delas, aliás, é justamente essa daí de cima.
🛒 Qual tomada eu uso (e o detalhe que muda tudo)
Antes do código, o hardware — porque tem um detalhe aqui que, se você errar na compra, derruba metade deste artigo. 🙈
As minhas são estas:
Plugue padrão brasileiro, 10 A, Wi-Fi 2,4 GHz, app Smart Life ou Tuya, e funciona com Alexa e Google.
Agora o detalhe: tem que ser o modelo "com medidor de consumo".
Existe um monte de tomada inteligente barata que só liga e desliga — e uma
dessas te dá uma entidade no Home Assistant, o
switch, e mais nada. Todo este artigo lê
sensor.*_energia, sensor.*_voltagem e
sensor.*_corrente: se a sua tomada não mede, esses três sensores
simplesmente não existem e o meu código morre com
ENTIDADE_AUSENTE.
🔒 Por que eu não usei o localtuya
Essa é a pergunta que todo mundo faz — e com razão, porque a resposta óbvia
seria: "se você quer parar de depender da nuvem, instala o
localtuya e pronto". O localtuya é uma integração
alternativa que fala com o dispositivo direto na sua rede, sem
passar pela internet. Seria perfeito.
Eu não usei por um motivo bem prosaico: estas tomadas não deixam. 😤
O localtuya depende de o dispositivo aceitar conexão TCP na porta
6668 da rede local. As minhas se anunciam por
broadcast UDP — elas gritam "estou aqui!" na rede, o que dá uma falsa esperança
danada — mas quando você tenta conectar, não atendem a 6668 e nem
respondem ARP. O firmware delas (product_id
nlqc2zwgpjypop2p) simplesmente não expõe o serviço local.
E não existe flag escondida, opção no app ou firmware alternativo oficial para ligar isso. Eu procurei. Não procure. 🙃
Ou seja: no meu caso a escolha nunca foi "nuvem versus local" — foi "nuvem direto versus nuvem passando pelo HA". Se controle local é requisito de verdade, a saída não é software: é trocar o hardware. Volto nisso no fim do artigo, porque é para lá que eu vou. 🔮
Vale dizer: se a sua tomada for um modelo que aceita
localtuya, vá de localtuya. Você ganha o que eu não
ganhei, e a segunda armadilha deste artigo — a do carimbo —
provavelmente nem te atinge, porque ela é um comportamento da integração Tuya
oficial. As outras lições continuam valendo. 💜
🔑 O token e a primeira chamada
A API REST do Home Assistant se autentica com um token de longa duração. Você gera na sua conta: clique no seu usuário (canto inferior esquerdo) → aba Segurança → Tokens de acesso de longa duração → criar. Ele aparece uma única vez — copie na hora, porque não dá para ver de novo.
Com o token na mão, a chamada que importa é uma só:
curl -s http://SEU-HA:8123/api/states \
-H "Authorization: Bearer SEU_TOKEN" \
-H "Content-Type: application/json"
/api/states devolve todas as entidades da sua
instalação de uma vez. Na minha são 115. E aqui já tem uma decisão de projeto
que vale a pena: existe também o /api/states/<entity_id>, que
traz uma entidade só. Eu não uso.
Por quê? Porque uma tomada, para mim, são quatro entidades (já já explico). Se eu buscasse uma por vez, seriam quatro requisições — e, pior, quatro instantes diferentes. Quando eu gravo o snapshot de uma queda de energia segundo a segundo, eu quero tensão, corrente e potência do mesmo momento. Uma chamada só resolve as duas tomadas e me dá coerência temporal de graça. 🎯
🔌 Uma tomada são quatro entidades
Esse foi o primeiro ajuste de cabeça. No mundo Tuya você pensa em "dispositivo". No Home Assistant não existe dispositivo na API de estados — existem entidades, e cada medida é uma entidade separada:
switch.pccritico_socket_1 → on (liga/desliga)
sensor.pccritico_energia → 273.6 (watts)
sensor.pccritico_voltagem → 205.5 (volts)
sensor.pccritico_corrente → 1.441 (ampères — repare)
Cada uma delas vem do /api/states assim — este é o JSON real de
uma das minhas:
{
"entity_id": "sensor.pccritico_corrente",
"state": "1.441",
"attributes": {
"state_class": "measurement",
"unit_of_measurement": "A",
"device_class": "current",
"friendly_name": "PcCritico Corrente"
},
"last_reported": "2026-08-05T22:25:49.544529+00:00"
}
Duas coisas para guardar dessa resposta. O state vem
como texto ("1.441", entre aspas), mesmo sendo um
número — por isso o script mais adiante converte e valida. E o
unit_of_measurement diz "A": ampères. Volto nisso
já já. ⚡
Repare no padrão dos nomes: todos derivam do mesmo prefixo,
pccritico. Então eu guardo no banco só o
entity_id do switch e derivo o resto:
// 'switch.pccritico_socket_1' -> 'pccritico'
function prefixoDe(entityId) {
const semDominio = String(entityId).split('.')[1] || '';
return semDominio.replace(/_socket_\d+$/, '');
}
// As 4 entidades que, juntas, formam UMA tomada.
function entidadesDe(entityId) {
const p = prefixoDe(entityId);
return {
ligado: entityId,
watts: `sensor.${p}_energia`,
volts: `sensor.${p}_voltagem`,
amperes: `sensor.${p}_corrente`,
};
}
O switch é a identidade canônica da tomada: é ele que eu comando, e é ele que a proteção valida. Os sensores são derivados. Se um dia algum sensor fugir da convenção, resolvo com um mapa explícito — mas enquanto os seis seguem o padrão, derivar é menos coisa para manter em sincronia.
⚡ O ampère que vira miliampère
Lembra do "unit_of_measurement": "A" naquele JSON? O Home
Assistant entrega corrente em ampères. O resto do meu sistema —
banco, gráficos, histórico de dois anos — usa miliampères,
porque é assim que a Tuya sempre entregou.
A conversão acontece dentro do adapter, num lugar só:
amperes: Math.round(amperes * 1000), // o HA entrega A; converta uma vez, aqui.
Parece detalhe bobo de uma linha, mas é o tipo de coisa que, espalhada, vira bug de madrugada. Se eu deixasse cada consumidor converter, bastaria um esquecer e eu teria um gráfico com um pico de mil vezes — ou, pior, um histórico com duas unidades misturadas na mesma coluna e nenhum jeito de saber qual linha está em quê. Uma ruptura de unidade no banco não tem rollback. 😰
🧰 O script completo
Node puro, sem nenhuma dependência — o fetch e o
AbortSignal.timeout já vêm no runtime. É só copiar:
/**
* tomada-ha.js — le uma tomada Tuya pelo Home Assistant, em Node puro.
* Uso: node tomada-ha.js switch.pccritico_socket_1
*/
'use strict';
const URL = process.env.HA_URL || 'http://192.168.0.10:8123';
const TOKEN = process.env.HA_TOKEN || '';
const TIMEOUT = Number(process.env.HA_TIMEOUT_MS || 8000);
// O HA emite estes estados quando PERDE o dispositivo. E o sinal de falha.
const INDISPONIVEIS = ['unavailable', 'unknown', 'none', ''];
class ErroHa extends Error {
constructor(motivo, mensagem) {
super(mensagem);
this.motivo = motivo; // INDISPONIVEL | AUSENTE | VALOR | AUTH | HTTP | REDE
}
}
/** As 4 entidades de uma tomada, derivadas do entity_id do switch. */
function entidadesDe(entityId) {
const p = entityId.split('.')[1].replace(/_socket_\d+$/, '');
return {
ligado: entityId,
watts: `sensor.${p}_energia`,
volts: `sensor.${p}_voltagem`,
amperes: `sensor.${p}_corrente`,
};
}
/** Uma chamada traz TODAS as entidades, do mesmo instante. */
async function buscarEstados() {
let res;
try {
res = await fetch(`${URL}/api/states`, {
headers: { Authorization: `Bearer ${TOKEN}` },
signal: AbortSignal.timeout(TIMEOUT),
});
} catch (e) {
throw new ErroHa('REDE', `nao consegui falar com o HA: ${e.message}`);
}
if (res.status === 401 || res.status === 403) throw new ErroHa('AUTH', 'token recusado pelo HA');
if (!res.ok) throw new ErroHa('HTTP', `HA respondeu HTTP ${res.status}`);
return res.json();
}
/** Le um estado exigindo que exista e nao esteja indisponivel. */
function estadoDe(porId, id) {
const e = porId.get(id);
if (!e) throw new ErroHa('AUSENTE', `entidade ausente no HA: ${id}`);
if (INDISPONIVEIS.includes(String(e.state).toLowerCase())) {
throw new ErroHa('INDISPONIVEL', `${id} esta "${e.state}" — dispositivo perdido`);
}
return e;
}
/** Converte para numero. Number(" ")===0 e Number("N/A")===NaN passariam batido. */
function numero(e) {
const n = Number(e.state);
if (!Number.isFinite(n) || String(e.state).trim() === '') {
throw new ErroHa('VALOR', `${e.entity_id} nao e numerico: "${e.state}"`);
}
return n;
}
async function lerTomada(entityId) {
const estados = await buscarEstados();
const porId = new Map(estados.map((e) => [e.entity_id, e]));
const alvo = entidadesDe(entityId);
const ligado = estadoDe(porId, alvo.ligado);
const watts = numero(estadoDe(porId, alvo.watts));
const volts = numero(estadoDe(porId, alvo.volts));
const amperes = numero(estadoDe(porId, alvo.amperes));
const carimbo = Date.parse(ligado.last_reported || ligado.last_updated);
return {
ligado: ligado.state === 'on',
watts,
volts,
amperes: Math.round(amperes * 1000), // o HA entrega A; converta uma vez, aqui.
idadeS: Math.round((Date.now() - carimbo) / 1000),
};
}
/** Liga/desliga. Aqui e onde a protecao do PcCritico precisa entrar. */
async function comandar(entityId, ligar) {
const res = await fetch(`${URL}/api/services/switch/turn_${ligar ? 'on' : 'off'}`, {
method: 'POST',
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ entity_id: entityId }),
signal: AbortSignal.timeout(TIMEOUT),
});
if (!res.ok) throw new ErroHa('HTTP', `HA respondeu HTTP ${res.status} ao comandar`);
return true;
}
async function principal() {
const alvo = process.argv[2] || 'switch.pccritico_socket_1';
try {
const t = await lerTomada(alvo);
console.log(alvo);
console.log(` ligada : ${t.ligado ? 'sim' : 'nao'}`);
console.log(` potencia : ${t.watts} W`);
console.log(` tensao : ${t.volts} V`);
console.log(` corrente : ${t.amperes} mA`);
console.log(` carimbo : ${t.idadeS} s atras`);
} catch (e) {
if (!(e instanceof ErroHa)) throw e;
console.error(`FALHA [${e.motivo}] ${e.message}`);
process.exitCode = 1;
}
}
if (require.main === module) principal();
module.exports = { lerTomada, comandar, entidadesDe, ErroHa };
Rodando contra o meu HA, agora há pouco:
$ node tomada-ha.js switch.pccritico_socket_1
switch.pccritico_socket_1
ligada : sim
potencia : 273.6 W
tensao : 205.5 V
corrente : 1441 mA
carimbo : 41966 s atras
$ node tomada-ha.js switch.pcnormal_socket_1
switch.pcnormal_socket_1
ligada : sim
potencia : 42.6 W
tensao : 205.4 V
corrente : 390 mA
carimbo : 41966 s atras
Os 1441 mA ali são o 1.441 ampère daquele JSON, já
convertido. E olha o campo carimbo: 41.966
segundos. Onze horas e meia! Numa tomada que está ligada, medindo, e
alimentando o computador em que eu escrevo isto. Guarde essa estranheza — ela é
a segunda armadilha, e eu volto nela. 👀
☁️ Mentira nº 1: o HA não me tirou da nuvem Tuya
Essa doeu no orgulho. 😳
Eu entrei nesse trabalho com uma expectativa que nunca cheguei a escrever, de
tão óbvia que parecia: saindo da nuvem Tuya, eu fico independente da nuvem
Tuya. Faz sentido, né? Tirei as credenciais, tirei a assinatura, tirei o
devices.json. Logo, tirei a dependência.
Não tirei nada. Fui verificar qual integração o HA usava para falar com essas
tomadas e é a integração Tuya oficial — a de nuvem. O
localtuya não está nem instalado. Ou seja, o caminho não encurtou:
antes: meu sistema → nuvem Tuya → tomada
depois: meu sistema → HA (LAN) → nuvem Tuya → tomada
Mesma nuvem, mesma cota do IoT Core, um salto a mais. Ou
seja: aquela cota estourada que me deixou dez horas no escuro
(code 28841004)? Se acontecer de novo, o HA para
junto — ele é cliente da mesma API que eu era. Eu tinha trocado de
porta, não de dependência. E se a internet cair, os dois caminhos morrem
igual.
E, como contei lá em cima, trocar pelo localtuya não era opção:
essas tomadas recusam conexão na rede local, então não havia caminho local para
seguir mesmo que eu quisesse.
Então o HA valeu a pena? Valeu — só que pelos motivos
certos, não pelo que eu imaginava: menos segredo no meu .env, menos
código de protocolo meu para manter, todas as medidas num request só, e um ponto
único de integração para o dia em que entrar hardware local. Nenhum desses é
"independência da nuvem". Escrevi isso no documento de projeto antes de
implementar, justamente para o eu-do-futuro não recomeçar a fantasia.
⏰ Mentira nº 2: frescor não se mede por carimbo
Se você só for ler um pedaço deste artigo, leia este. 🙏
Todo dado de sensor precisa de uma pergunta respondida: isso aqui ainda é verdade, ou é lixo velho? No meu caso a pergunta é séria — se as leituras congelarem numa queda de energia, o sistema acha que está tudo bem enquanto a bateria morre.
A resposta parecia trivial. Todo estado do HA vem com
last_updated, last_changed e
last_reported. Era só medir a idade: passou de dois minutos, é
falha. Foi assim que eu desenhei. 📐
Antes de codar, resolvi medir os valores reais. Ainda bem:
switch.pccritico_socket_1 on 42057s
switch.pcnormal_socket_1 on 42057s
sensor.pccritico_energia 299.8 15s
sensor.pccritico_voltagem 200.9 15s
sensor.pcnormal_energia 42.6 1256s
sensor.natal_energia unavailable 42057s
switch.natal_socket_1 unavailable 42057s
Olha o switch.pccritico_socket_1: ligado, perfeitamente saudável,
alimentando o computador em que eu estou trabalhando agora — e com carimbo de
42.057 segundos. Quase doze horas! 🤯
Meu limiar de dois minutos teria declarado tudo como falha permanente. O sistema entraria em modo degradado e ficaria lá para sempre.
A causa: a integração Tuya do HA é push-based. O carimbo só
se renova quando o valor muda. Aquele switch está
on desde ontem — então o carimbo é de ontem. Ele não é velho, ele é
estável. E eu testei os três campos: last_changed,
last_updated e last_reported se comportam igual.
Repare também na assimetria entre as duas tomadas, que é o que mata qualquer tentativa de calibrar um limiar: a PcCritico puxa ~300 W de um computador que oscila o tempo todo, então renovou há 15 s. A PcNormal puxa 42 W estáveis e está parada há 1.256 s — vinte minutos, completamente sadia. Qualquer número que eu escolhesse olhando para uma daria falso positivo na outra.
E o sinal certo estava ali o tempo todo. Vê a
natal, no fim da lista? É uma tomada de Natal que eu tinha
desconectado da parede. O HA não deixou o último valor dela envelhecendo em
silêncio — ele marcou unavailable, explicitamente. É isso que o HA
faz quando perde um dispositivo.
Então a regra de falha não olha idade nenhuma:
state = unavailable | unknown → falha de comunicação
HA não responde / timeout / 401 → falha de comunicação
entidade ausente na resposta → falha de comunicação
caso contrário → dado bom (mesmo com carimbo de 10 horas)
Cada caso classificado por tipo explícito, nunca por regex na
mensagem de erro. Eu já tinha me queimado com isso: o código antigo da Tuya
detectava problema de credencial com um /key|sign|token/ na string,
e por isso não reconhecia a cota estourada — ela chegava como
falha genérica e eu perdia tempo procurando no lugar errado.
Todos os caminhos de erro do script acima eu rodei de verdade:
$ node tomada-ha.js switch.natal_socket_1
FALHA [INDISPONIVEL] switch.natal_socket_1 esta "unavailable" — dispositivo perdido
$ node tomada-ha.js switch.fantasma_socket_1
FALHA [AUSENTE] entidade ausente no HA: switch.fantasma_socket_1
$ HA_TOKEN=errado node tomada-ha.js switch.pccritico_socket_1
FALHA [AUTH] token recusado pelo HA
$ HA_URL=http://192.168.0.99:8123 node tomada-ha.js switch.pccritico_socket_1
FALHA [REDE] nao consegui falar com o HA: The operation was aborted due to timeout
🧟 A exceção: quando o carimbo volta a valer
Duas exceções, na verdade — e as duas são instrutivas.
A primeira: ao confirmar que um comando funcionou, o carimbo
volta a ser critério legítimo. É o caso oposto ao da leitura passiva: um
turn_off bem-sucedido muda o estado, logo o carimbo
tem que renovar. Sem exigir isso, eu confirmaria o sucesso lendo o
estado anterior em cache e reportaria "desligou!" para um comando que talvez nem
tenha chegado na tomada.
A segunda me incomodou mais. Se descartei a idade como sinal de falha, o que
acontece se a VM do HA travar servindo cache? HTTP 200, valores
numéricos plausíveis, nada unavailable — e dados de duas horas
atrás. Meu sistema não perceberia nada. Numa queda real, ele ficaria olhando
para uma foto. 😱
A solução foi um aviso que não desarma nada:
// Carimbo mais RECENTE entre as tomadas: se nem a mais ativa renovou dentro do
// limiar, TODAS estão paradas — aí sim é sinal de VM travada, não de carga estável.
const carimboMaisRecente = Math.max(...carimbos);
const idadeMaxS = (Date.now() - carimboMaisRecente) / 1000;
Esse Math.max é a parte importante, e eu errei antes de acertar.
Com Math.min, a tomada mais quieta ditaria o alarme — e a PcNormal,
que legitimamente fica 1.256 s parada, dispararia falso positivo o dia inteiro.
Uma tomada quieta é normal. As duas quietas ao mesmo tempo é
que é suspeito: é o sinal de que a VM inteira congelou.
E ele só avisa: registra um log e um evento, uma vez, na transição — não em loop. Não marca nada como sem comunicação, não deixa de gravar as leituras, não desarma proteção nenhuma. Aqui um falso positivo custaria mais caro que o risco que estou cobrindo.
🛡️ A proteção que esvaziou em silêncio
Agora a parte em que eu quase estraguei tudo. 😬
O meu sistema tem uma regra inegociável: a tomada do computador crítico nunca desliga. Por nenhum caminho — nem pela tela, nem pela API, nem pela automação de queda de energia. Isso é garantido em três camadas independentes, cada uma capaz de barrar sozinha: uma flag no banco, uma validação no serviço, e uma última checagem dentro do adapter, imediatamente antes do POST sair para a rede.
Essa terceira camada compara o identificador do dispositivo contra uma lista de protegidos:
async setSwitch(entityId, ligar) {
validarProtecaoAdapter(entityId, !!ligar, this._idsProtegidos);
const servico = ligar ? 'turn_on' : 'turn_off';
return this._post(`/api/services/switch/${servico}`, { entity_id: entityId });
}
Eu portei essa camada para o adapter novo, escrevi os testes, tudo verde. Menos mal que fui conferir de onde vinha a tal lista de protegidos:
const list = await prisma.dispositivo.findMany({
where: { protegido: true, removido: false },
select: { chave: true, deviceIdExterno: true }, // ← e o entityIdHa?
});
Achou? A lista era montada com o deviceIdExterno — o
identificador da Tuya, tipo bf1a2b3c.... Mas o
adapter do HA valida entity_id, tipo
switch.pccritico_socket_1.
A comparação é string contra lista. Elas nunca coincidiriam. A camada 3 rodaria a cada comando, compararia diligentemente contra uma lista que nunca dá match, e deixaria passar tudo. Uma proteção que executa, não reclama, e vale exatamente zero.
E o pior: silenciosamente. Sem erro, sem log, sem teste
vermelho. Um select ao qual faltava uma palavra. A correção foi
literalmente acrescentar entityIdHa: true ali — e fazer a coleta
perguntar à fonte ativa qual identidade usar, em vez de assumir a da Tuya.
🚨 Falhar alto no boot, não baixinho na queda
Esse susto mudou como o sistema arranca. Havia uma tentação de projeto muito razoável: se a configuração do HA estiver incompleta, cai de volta para a Tuya, que sempre funcionou. Um fallback gentil.
É uma péssima ideia — e o motivo é exatamente o bug de cima. Um fallback silencioso significaria operar na nuvem direta acreditando estar no HA. A lista de protegidos sairia montada com a identidade errada e a camada 3 ficaria vazia. Eu descobriria isso durante uma queda de energia.
Então o backend simplesmente não sobe:
TOMADAS_FONTE=ha, mas a configuracao esta incompleta:
- HA_TOKEN vazio no .env
- medidores sem entity_id_ha no banco: PC_NORMAL
Corrija ou volte para TOMADAS_FONTE=tuya. O backend nao sobe pela metade.
Ele checa o que precisa e aborta com exit 1 se faltar: URL, token,
e entity_id em todo medidor ativo. Inclusive o caso em que a lista
de medidores vem vazia — porque subir com zero protegidos é a mesma coisa que
subir sem proteção, só que mais difícil de perceber.
Repare que essa dureza é só para erro de configuração. Falha de hardware — a porta serial do nobreak ocupada, o NAS fora do ar — o sistema tolera de propósito e sobe mesmo assim, porque aí é a realidade sendo imperfeita, não eu tendo digitado errado. São categorias diferentes de problema e merecem tratamentos diferentes.
Falha ruidosa no boot acontece uma vez e você conserta. Falha discreta é descoberta durante a queda de energia. 🔊
🔀 Manter os dois caminhos
Um detalhe de arquitetura que valeu muito: eu não apaguei o
código da Tuya. Poller e executor não importam mais um adapter concreto — eles
pedem a uma fábrica, que lê a variável TOMADAS_FONTE no boot e
devolve o adapter certo. Os dois implementam o mesmo contrato
(getStatus, interpretar, setSwitch,
definirProtegidos), então nada mais no sistema sabe qual fonte está
ativa.
Trocar de fonte é editar uma linha do .env e reiniciar. Se o HA
me der dor de cabeça às duas da manhã, eu volto para a nuvem direta sem
deploy, sem rollback, sem drama. O caminho de volta é uma
funcionalidade, não código morto.
E a fonte ativa aparece no painel, na tela. Porque se alguém subir o sistema
com um .env antigo, a tela é onde isso fica visível — sem ela, só o
log responde, e log ninguém lê antes de precisar.
🧭 O que eu levaria para o seu caso
Se você vai gerenciar Tuya (ou qualquer coisa) pelo Home Assistant por código:
- Compre tomada "com medidor de consumo" — sem isso você tem um liga/desliga, não uma fonte de telemetria.
- Se controle local é requisito, confira o firmware antes —
nem toda Tuya aceita
localtuya, e isso não se resolve por software. Para sair da nuvem de verdade, Tasmota já instalado de fábrica. - Uma chamada a
/api/states, não uma por entidade — mais rápido e com todos os valores do mesmo instante. - Descubra qual integração está por baixo antes de assumir que você ficou local. Oficial = nuvem.
- Não meça frescor por carimbo em integração push-based. Use o
unavailable, que o HA emite de graça quando perde o dispositivo. - Converta unidade num lugar só, dentro do adapter.
- Valide os números de verdade —
Number(" ") === 0e esse zero falso é convincente demais. - Classifique erro por tipo, nunca por regex na mensagem.
- Se houver proteção, teste que ela bloqueia — e confira que está comparando a identidade certa.
O trabalho todo levou bem mais que a tarde que eu tinha estimado. 😅 Mas metade do valor não veio do código: veio de duas medições que desmentiram o que eu tinha certeza. Se eu tivesse implementado o limiar de dois minutos direto do desenho, teria um sistema que se declara quebrado o tempo todo — e eu iria procurar o defeito no Home Assistant, não na minha suposição.
Meça antes. O hardware mente, e a documentação não conta onde. 💜
🔮 Plano futuro: sair da nuvem de vez, com Tasmota
Termino com o que vem por aí — porque este artigo tem um final em aberto. 😄
Repare que, das duas mentiras, a primeira continua de pé: eu troquei de porta, não de dependência. Aquela cota que me deixou dez horas cega pode estourar de novo amanhã, e o Home Assistant vai parar junto comigo. Resolver isso de verdade exige o que eu disse lá atrás — trocar o hardware.
Tasmota é um firmware alternativo de código aberto para os chips ESP8266/ESP8285, que é o que mora dentro de boa parte dessas tomadas inteligentes. Trocando o firmware de fábrica por ele, a tomada para de falar com a nuvem do fabricante e passa a falar MQTT direto na sua rede. Sem conta, sem token de nuvem, sem cota, sem intermediário. 🎉
Só que regravar firmware dá trabalho: dependendo do modelo, envolve abrir a tomada e soldar fios num adaptador serial. Por isso eu estou de olho nestas, que já vêm com o Tasmota instalado de fábrica:
Plugue brasileiro, 16 A (mais folga que os meus 10 A), medição de consumo em tempo real e integração direta com o Home Assistant. Vêm da China, então conte com algumas semanas de espera.
O caminho que eu quero é este:
no começo: meu sistema → nuvem Tuya → tomada
hoje: meu sistema → HA (LAN) → nuvem Tuya → tomada
plano: meu sistema → HA (LAN) → MQTT (LAN) → tomada
Aí sim a nuvem sai da equação, e o ganho que eu achei que teria lá na Mentira nº 1 finalmente acontece de verdade. Numa queda de energia com o roteador fora, a tomada continua alcançável — nada precisa sair de casa. 🏠
E olha só onde a arquitetura deste artigo se paga: como o poller e o executor pedem o adapter a uma fábrica, entrar com uma terceira fonte é escrever um adapter novo que cumpra o mesmo contrato. Não é reformar o sistema — é acrescentar um arquivo. Foi sem querer que eu preparei o terreno para a próxima mudança enquanto fazia esta. 😊
Quando elas chegarem, eu conto aqui como foi. 💜
💾 O repositório
O script está lá inteiro, e o README traz os seis caminhos de erro com a
saída real de cada um — inclusive o unavailable, que chega com
HTTP 200 e cara de resposta boa:
Por hoje é só, meus unicórnios! 🦄✨
Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟
Leia também
Node.js: alterando o plano de energia do Windows
Como ler e trocar o plano de energia do Windows com Node.js e powercfg: por que resolver por GUID, e o acento que o TextDecoder nao decodifica.
Node.js: exportando grandes volumes do Elasticsearch
Como exportar milhões de registros do Elasticsearch para CSV com Node.js: a parede dos 10.000, a Scroll API e o scroll que fica aberto.
Node.js: gerando certificado SSL e instalando no IIS
Como emitir um certificado gratuito do Let's Encrypt com Node.js e instalar no IIS do Windows, com as armadilhas que travaram tudo.