Correções no flow da AMT para o Node-RED 5 (add-on v22) — e como resolver os entity_id com _2
Atualizei o add-on do Node-RED e o alarme parou. Fui atrás do motivo e achei que valia devolver para a thread, porque a
causa principal vai quebrar o flow de todo mundo que atualizar o add-on — não é nada específico da minha instalação.
Tenho uma AMT 2018 e uso o flow desta thread há um bom tempo. Depois de atualizar o add-on do Node-RED para a versão 22
(que traz o Node-RED 5), o alarme ficou permanentemente em “Central Desconectada” e todos os sensores foram para
indisponível.
Importante: antes de qualquer coisa, se o seu flow já está funcionando, não substitua o seu flows.json pelo de
outra pessoa — nem pelo meu. O unique_id de cada entidade criada pelo Node-RED é
nodered-{id do nó server}-{id do nó ha-entity-config}, e esses ids vivem dentro do arquivo. Trocar o arquivo inteiro
troca os ids, e o Home Assistant cria um conjunto novo de entidades enquanto as antigas continuam ocupando os entity_id
bons. Resultado: _2 em tudo, que é justamente o problema descrito mais abaixo. Aplique as correções no seu próprio
flow, nó a nó. É chato, mas é o caminho seguro.
Deixou aqui um gist
com meu flow.json, para quem estiver começando do zero ou quiser derrubar tudo e começar de novo.
O problema principal: o $moment parou de funcionar
O add-on v22.0.0 subiu o Node-RED da versão 4.1 para a 5.0, e o Node-RED 5 traz o jsonata 2.2. O jsonata 2.2 recebeu
um endurecimento de segurança por causa do CVE-2026-12208
(PR #794) que mudou a navegação de propriedades em objetos JavaScript:
agora ele só resolve propriedade própria do objeto.
O flow usa, em 37 lugares, esta expressão jsonata:
$moment(time).tz("America/Sao_Paulo").format('D MMM HH:mm:ss - ')
O $moment(...) devolve um objeto moment, e os métodos .tz() e .format() ficam no prototype, não como propriedade
própria. Ou seja, passaram a resolver para undefined, e chamar undefined como função dá isso aqui:
Invalid JSONata expression: Attempted to invoke a non-function
Instalei as duas versões do jsonata lado a lado para ter certeza de que era isso mesmo:
jsonata 2.0.6 (Node-RED 4) OK "31 Jul 14:16:30 - "
jsonata 2.2.2 (Node-RED 5) FALHA T1006 Attempted to invoke a non-function
Por que uma data quebra o alarme inteiro
Aqui está o pulo do gato, e foi o que me custou mais tempo para entender. Uma dessas expressões está no nó
SENHAS e IP da central!!!!, que é um nó change com 9 regras. A regra 8 põe flow.check_status = false e a regra
9 é justamente a do $moment.
O nó change aplica as regras em ordem e, quando uma falha, ele chama done(err) e não envia a mensagem. Então, a
cada boot:
- o
check_status vai para false;
- o nó morre na regra seguinte;
- o nó
Comandos nunca roda — e é ele que monta todos os buffers de comando e define flow.host, flow.port e
flow.comando_status.
Daí para frente é efeito cascata, e são exatamente os erros que aparecem no debug:
Conexão TCP: “Host and/or port not set”, porque flow.host e flow.port não existem;
Simple triggered queue: TypeError: Cannot read properties of undefined (reading '0'), porque o inject de
STATUS dispara a cada 0,33 s com o msg.payload vindo de flow.get("comando_status"), que está undefined;
- e, como nenhuma requisição chega na central, o watchdog de 5 s estoura e posta “Central Desconectada”, sempre
exatamente 5 segundos depois do boot.
A correção
Trocar as 37 ocorrências por jsonata puro, sem depender do moment:
$fromMillis($millis(), '[D] [MNn,3-3] [H01]:[m01]:[s01] - ', '-0300')
Testei com um timestamp fixo contra o moment-timezone e a saída é idêntica. Também funciona tanto no jsonata 2.0.6
quanto no 2.2.2, então serve para quem ainda está no Node-RED 4. O -0300 fixo está correto: o Brasil acabou com o
horário de verão em 2019, então America/Sao_Paulo não muda mais de offset.
Onde procurar no flow: o nó SENHAS e IP da central!!!!, um nó change no grupo “Nova tentativa em caso de falha no
boot” e a propriedade de saída time de todos os nós ha-button e ha-switch.
Atenção: não adianta consertar só o nó change e parar por aí. Fui olhar o OutputController da paleta e a
avaliação da expressão acontece fora do try/catch, então uma expressão que estoura rejeita o handler inteiro e o
nó nunca envia a mensagem. Na prática, com só o change consertado, os botões de armar, desarmar, PGM e sirene
continuariam sem funcionar.
O outro problema: entidades duplicadas com _2
Essa parte não é culpa do flow, é do jeito que o Home Assistant guarda o registro de entidades. Mas quem mexer nas
entidades do Node-RED vai esbarrar nisso, e eu perdi bastante tempo aqui.
Tentei limpar as entidades para recomeçar do zero e elas voltavam sempre com os mesmos nomes, inclusive com o sufixo
_2 que eu estava justamente tentando remover. Até uma renomeação manual que eu tinha feito sobrevivia a tudo.
A explicação está no entity_registry.py do Home Assistant. Quando você apaga uma entidade, ela não é descartada:
vai para uma lista deleted_entities, chaveada por domínio + plataforma + unique_id, guardando o entity_id e o nome.
Quando algo registra o mesmo unique_id de novo:
deleted_entity = self.deleted_entities.pop((domain, platform, unique_id), None)
if deleted_entity is not None:
...
# Restore entity_id if it's available
if self._entity_id_available(deleted_entity.entity_id):
entity_id = deleted_entity.entity_id
...
name = deleted_entity.name
E o unique_id das entidades criadas pelo Node-RED é nodered-{id do nó server}-{id do nó ha-entity-config}, ids que
estão fixos dentro do flows.json. Ou seja: apagar e deixar o Node-RED recriar sempre devolve o entity_id antigo.
Apagar entidade no Home Assistant nunca libera o nome.
Outros detalhes que também me custaram tempo:
- Editar o
.storage/core.entity_registry com o Home Assistant ligado não adianta. O registro fica em memória e é
regravado por cima. E o deleted_entities é uma lista separada dentro do mesmo arquivo, então filtrar linha por linha
não resolve nada.
- Apagar o dispositivo não apaga entidade que não está pendurada nele, e apagar a entrada da integração não apaga
entidade sem config_entry_id. Registros meio órfãos sobrevivem aos dois.
- Só dá para apagar entidade que não está sendo fornecida no momento. Com o Node-RED rodando, o botão “Excluir” fica
cinza e a seleção em massa não faz nada. É preciso parar o add-on primeiro e esperar as entidades ficarem
unavailable.
- Entidades “não gerenciáveis”, sem unique ID, são outro bicho. Elas são criadas pelo
python_script.set_state, que
usa hass.states.set() e passa por fora do registro. O nó Indisponível do flow faz exatamente isso. Se você apagar
as entidades com o Node-RED rodando, o watchdog dispara e recria todas elas como states órfãos, que aí ocupam
os entity_id bons e forçam o _2. Elas somem sozinhas ao reiniciar o Home Assistant, porque o hass.states.set() não
persiste nada.
Como resolver de verdade
O jeito que funciona é trocar os unique_id, e para isso basta mudar o id do nó server no flows.json — ele entra
no unique_id de todas as entidades de uma vez. Aproveite e troque também o id do nó ha-device-config, para ganhar um
dispositivo novo, sem o nome customizado antigo.
Registros em deleted_entities não bloqueiam um entity_id, eles só restauram para o mesmo unique_id. Isso está
literalmente na docstring: “an entity_id which belongs to a deleted entity is considered available”. Então, com
unique_id novo, nada é restaurado e os nomes ficam limpos.
Passo a passo:
- Pare o add-on do Node-RED.
- Apague as entidades e o dispositivo da integração Node-RED no Home Assistant.
- No
flows.json, troque o id do nó server e o do ha-device-config por dois hex de 16 caracteres novos,
atualizando todas as referências. No meu caso foram 163 e 157 ocorrências; um grep -c antes e depois evita
susto.
- Reinicie o Home Assistant.
- Suba o Node-RED.
Duas coisas que eu aprendi tarde demais:
- Nunca use o Import do editor para atualizar o flow. Quando há conflito, o Node-RED só oferece “Import copy”, que
gera id novo para todos os nós. A caixa “Replace” fica escondida para flows e para qualquer nó do canvas, ela só
aparece para nós de configuração. Como o unique_id depende dos ids dos nós, cada import cria um conjunto inteiro de
entidades novas. Foi assim que eu me enfiei nesse buraco. O certo é parar o add-on e trocar o arquivo
/addon_configs/a0d7b954_nodered/flows.json direto.
- Se você apagar entidades e elas não voltarem, a integração companheira guarda um conjunto
ALREADY_DISCOVERED em
memória. Quando o Node-RED reanuncia uma entidade que já está nesse conjunto, a integração manda um “update” para um
objeto que não existe mais e não cria nada. Reiniciar o Node-RED não resolve, porque o estado está do lado do Home
Assistant: é preciso recarregar a integração Node-RED (ou reiniciar o HA) antes de fazer o Node-RED reanunciar.
Depois de tudo isso os entity_id vêm com o prefixo do nome do dispositivo, por exemplo
binary_sensor.amt_central_amt_zona_01 e sensor.amt_central_amt_ack. Não esqueça de atualizar as referências nas
automações, no template do alarm_control_panel e nas configurações de Alexa e Google.
Outras correções que fiz no flow
Aproveitando a viagem, achei mais algumas coisas que valem para todo mundo.
O Status Switch mandava mensagem para a saída errada. O nó monta um array e faz node.send(valuesToSend), onde a
posição define a saída — mas ele pulava as chaves não definidas, encurtando o array. Com qualquer chave ausente,
todas as mensagens seguintes vão para o switch errado (por exemplo, o pgm_1 caindo na saída da Partição C). Corrigi
empurrando null nas posições ausentes, que é o idioma do Node-RED para “não envia nada nesta saída”.
Partições C e D na AMT 2018. Os ramos AMT 2018 1016 e AMT 2018 SMART fixam msg.partition_c = true e
msg.partition_d = true, porque a central não tem essas partições. O efeito colateral é que os switches C e D ficam
sempre ligados e não dá para desligar (a cada 0,33 s o status reescreve), e as duas aparecem sempre em “Partições
Ativadas”. Coloquei uma flag particoes_cd_existem, que faz o Status Switch não acionar esses switches e o
Sensores Adicionais não contar essas partições.
Se você for mexer nisso, um aviso: não tente deixar o state sem valor para a entidade ficar unknown. Eu tentei.
O SensorBaseController rejeita state undefined com InvalidPropertyValueError: Invalid state antes de mandar
qualquer coisa, e null também não serve, porque $not($boolean(null)) dá true e o sensor volta a dizer que está
ligado. Só valor booleano funciona.
Propriedades entityState depreciadas. A partir da versão 0.79 da paleta
node-red-contrib-home-assistant-websocket, o “state type” foi depreciado e substituído por um cast explícito. Deixar
o campo vazio cai num ramo de compatibilidade que sai na versão 1.0. Nos 11 nós ha-switch o valor precisa ser
boolean — é o que eles já produzem e é o que o resto do flow compara. Vale ajustar antes que quebre igual ao
$moment.
O Get Device ID pegava o primeiro dispositivo com fabricante “Intelbras” e parava. Se sobrar um dispositivo antigo
no registro, a lista de entidades é montada a partir do dispositivo errado, sem avisar nada.
Recuperação de boot. O flow zera o check_status para false no início e só define true lá na frente. Qualquer
falha no meio do caminho deixa tudo mudo para sempre, sem nova tentativa — foi exatamente o que aconteceu comigo.
Coloquei um inject de 30 s que reexecuta a cadeia de inicialização se o check_status continuar false, respeitando o
caso de o usuário ter desligado o switch de requisição de propósito.
Senhas fora do flows.json. Passei as senhas e o IP para /addon_configs/a0d7b954_nodered/amt_secrets.json,
carregado via functionGlobalContext no settings.js do add-on. O nó SENHAS e IP da central!!!! só copia o
global.amt.* para o msg.*. Assim dá para versionar o flow sem vazar senha. Confirmei que o add-on carrega esse
arquivo: o /etc/node-red/config.js faz require("/config/settings.js") e não sobrescreve o functionGlobalContext.
O trecho que vai no settings.js, substituindo o bloco functionGlobalContext vazio que já vem no arquivo (por volta da
linha 153):
functionGlobalContext: {
amt: (() => {
try {
return require("/config/amt_secrets.json");
} catch (e) {
console.error("[AMT] cannot read /config/amt_secrets.json:", e.message);
return {};
}
})(),
},
E o amt_secrets.json:
{
"host": "192.168.1.50",
"senha": "123456",
"senha_a": "123456",
"senha_b": "123456",
"senha_c": "",
"senha_d": ""
}
As senhas são strings, para não perder zero à esquerda. Se você não usa as partições C e D, deixe como "" em vez de
omitir a chave: omitir faz o String(undefined) virar a string "undefined" e gerar um buffer de lixo no comando. O
try/catch é de propósito, para o Node-RED continuar subindo se o arquivo faltar.
Nota: essa parte é opcional. Se preferir manter as senhas no flow como no original, é só deixar as seis primeiras
regras do nó SENHAS e IP da central!!!! como tipo string com os valores, em vez de tipo global.
Resumo para quem só quer voltar a funcionar
Se o seu alarme parou depois de atualizar o add-on do Node-RED, é o $moment. Substitua todas as ocorrências de
$moment(time).tz("America/Sao_Paulo").format('D MMM HH:mm:ss - ')
por
$fromMillis($millis(), '[D] [MNn,3-3] [H01]:[m01]:[s01] - ', '-0300')
no nó SENHAS e IP da central!!!!, no change do grupo de nova tentativa e na propriedade time de todos os
ha-button e ha-switch. Faça backup do flow antes.
E de novo, porque é o erro mais caro de cometer aqui: edite o seu próprio flow, não troque o arquivo inteiro pelo de
outra pessoa. Trocar o arquivo muda os ids dos nós, e isso recria todas as suas entidades no Home Assistant com sufixo
_2.
É isso aí, valeu, espero ter ajudado!