Integrar sistema de controle de piscina ao Home Assistant

Alguem teve sucesso em integrar algo da Tholz com o HA?

Já tentei várias vezes verificar com eles uma API, mas infelizmente não disponibilizam.

Mesmo que em nuvem, seria muito bom ter acesso ao home Assistant.

1 curtida

Achei esse tópico e gostaria de explicar algumas coisas sobre os equipamentos da Tholz, eu fiz minha casa e me rendi a usar os sistemas de Aquecimento e Piscina deles Wifi, com finalidade de posteriormente usar MITM para construir uma integração com HA, oque não foi possível devido algumas coisas, vou explanar abaixo.(Lembrando meu background é Cloud Engineer e não tenho conhecimento em engenharia reversa)

  1. O App está em AWS Amplify - framework para construção de app mobile da AWS
  2. O app utiliza (o pior) serviço da AWS para Autenticação, o AWS Cognito que basicamente gera credenciais temporarias para você utiliar/logar no serviço.
  3. Aqui as coisas começam a complicar, não é simplesmente “Usa wireshark para pegar as requisições”, por 2 motivos
    3a. Certificado, você precisa usar engenharia reversa para burlar a captura de pacotes
    3b. O app não utiliza HTTPS/Rest para chamada nenhuma.
  4. O app Utiliza invés de REST/HTTPS um serviço chamado - AWS IOT, que não é nada mais que um MQTT da AWS para utilização de devices IOT porém utilizando websocket, nesse caso os equipamentos da Tholz

Bom já explanado o App da Tholz, agora oque foi oque consegui até agora:

  1. Descobrir os itens acima
  2. Consegui desencriptar a comunicação do App usando um self-signed certificate, ou seja o poonto 3.a da lista acima

Tirando essa parte não consegui avançar, coisas que faltam entendimento:

  • Como de fato o Cognito se comporta para autenticação, é um fluxo não tão trivial, eu mesmo não conheço a fundo esse serviço da AWS
  • Conseguir via engenharia reversa pegar as infos que ele manda para o Cognito autenticar, USERPOOL, Secret ID, etc.
  • Descobrir uma maneira de se conecar ao MQTT da AWS IOT utilizando as chaves autenticadas obtidas se por acaso conseguir autenticar manualmente fora do app.

Bom, se alguém quiser mais alguma info posso passar, por enquanto coloquei a idéia de lado pois não tenho tempo muito menos conhecimento em algumas areas desse problema, então se alguem manjar só falar podemos ver junto.

Exemplo de um fluxo de autenticação do Cognito:


Enfim, não é tão simples assim.

1 curtida

Eu acho que o caminho mais fácil seria fazer engenharia reversa do firmware pra ver os comandos que ele espera receber.

Eu quero controlar 1 boiler de agua com passagem por 1 aquecedor a gas.
Sera que um dispositivo desse seria ideal no lugar do tholz?

Você chegou a ver o que o dispositivo trafega na sua rede local? Talvez exista alguns comandos que de pra mandar direto pro dispositivo localmente, sem passar por todo esse processo de autenticação do app…

Já tentei, mas não consegui pegar pacote nenhum mas tentei pouco, acredito que eu saiba o motivo, por ser uma conexão com websocket acho que precisaria forçar um drop da rede e pegar o handshake do dongle wifi novamente com meu wifi. Estou tentando achar minha placa de rede wifi com monitor mode pra eu fazer um teste melhor com Kali + wireshark, porém sou leigo nesse assunto, se alguem quiser ajudar…

O meu tholz ainda vai ser montado, assim que for montado começo os trabalhos e trago notícias aqui

1 curtida

E se em vez de wireshark usar DNS local para direcionar requests do device pra uma proxy? Ai você consegue realmente interceptar os pacotes e tentar analisar o próprio device em vez do app.

Aqui um artigo que faz isso:

É da pra tentar usar um CharlesProxy também

DNSCHEF teoricamente ja faz isso no Kali também, ou até mesmo conectar o device em um fakehotspot, mas o problema não é esse.
Por mais que voce consiga fazer o spoof do DNS e intercepte o trafico voce vai esbarrar em TLS, por isso fui pelo caminho do APP aonde consegui retirar o TLS e fazer realmente o MITM mas no wifi dongle da tholz eu não tenho a minima ideia, enfim como disse sou leigo.

@rafaelrabaco quando instalarem o teu tholz me chama que tentamos algo

Entao mas usando uma proxy (eu tambem uso mitm pra fazer isso), pelo que eu entendo, o certificado tls usado é o da proxy. Como a proxy opera em camada 7, a proxy termina a conexao https original com o device e cria uma nova com o server, repassando as informacoes.

O problema é se o firmware esta configurado para aceitar so o certificado tls correto. Mas tem chance de não estar e ai poderia usar MITM junto com DNS spoofing.
Vale a tentiva.

Se não der certo uma outra opcao bem viavel é extrair o firmware e fazer disassembly. Provavelmente so olhando strings presentes no firmware é o suficinete para descobrir maior parte das informacoes sobre comunicacao.

Esse device é configurado por interface web?

Meu equipamento de energia solar da Hoymiles tem uma interface para configuração:

Vou tentar achar minha interface de monitor e ver se consigo algo com spoof, em termos de dump do firmware sinceramente nao tenho conhecimento e nem consigo desmontar o sistema da piscina/boiler pra fazer isso pois eles estão em funcionamento aqui em casa se tivesse uma sobrando e tempo sobrando tbm poderia tentar.

@srlima ele é feito via app, depois de autenticado ele cria uma rede wifi, aonde conecta e se auto configura com sua rede wifi

Se alguem aqui tiver a possbilidade de desmontar para extrair o firmware posso tentar ajudar. Não tenho experienca em extrair direto do chip de memoria como algumas pessoas fazem mas se der para baixar por serial ai acho que daria pra tentar.

1 curtida

Outra coisa que da para fazer (e nao sei como nao pensei nisso antes porque ja fiz isso com um app desktop), é engenharia reversa do APK.

Da para fazer decompile do APK para java, dependendo de como foi compilado vai estar obfuscado o codigo mas focando nas strings geralmente da para matar como é feito a comunicaçao. Dai daria para voce fazer uma integracao que conecta na AWS IOT como se fosse o app.

Vou instalar esse Tholz na piscina de casa mes que vem. Tem alguma outra marca/modelo que tem integracao facil com o HA?

Postaram recentemente em um servidor do Discord que a Tholz liberou a API para comunicar com alguns dos seus produtos.

Brinquei um pouco com o node red + chatgpt para processar parte das respostas. Mas já da pra criar alguns sensores.

  • Converte o índice de erros para texto
  • Converte o ID das saídas para texto
  • Converte o tipo de aquecimento e modo de operação para texto
  • Temperaturas convertidas para float

Segue o fluxo para quem quiser testar e continuar o desenvolvimento:

[{"id":"fbe3de3d88fb5cbe","type":"tab","label":"Tholz","disabled":false,"info":"","env":[]},{"id":"248bb8301b6ae862","type":"inject","z":"fbe3de3d88fb5cbe","name":"Definir IP","props":[{"p":"msg.port","v":"4000","vt":"num"},{"p":"msg.host","v":"192.168.0.2","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"command\":\"getDevice\"}","payloadType":"json","x":320,"y":100,"wires":[["498b8188ed61064e"]]},{"id":"9d3939d90df552c5","type":"inject","z":"fbe3de3d88fb5cbe","name":"Simular resposta","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"command\":\"getDevice\",\"response\":{\"id\":11462,\"reset\":0,\"error\":0,\"updating\":false,\"firmware\":\"P858V2\",\"firmware2\":\"P844V0\",\"timezone\":-10800,\"leds\":{\"led0\":{\"on\":false,\"onAut\":false,\"effect\":255,\"brightness\":48,\"speed\":50,\"type\":2,\"saturation\":66,\"color\":[163,255,40]}},\"heatings\":{\"heat0\":{\"type\":0,\"step\":1,\"t1\":325,\"t2\":328,\"t3\":322,\"t4\":321,\"minSp\":285,\"maxSp\":380,\"sp\":328,\"on\":false,\"onAut\":false,\"aux\":false,\"opMode\":0},\"heat1\":{\"on\":true,\"onAut\":true,\"opMode\":2,\"type\":1,\"step\":10,\"sp\":400,\"maxSp\":400,\"minSp\":150,\"aux\":null},\"heat2\":{\"on\":true,\"onAut\":true,\"opMode\":2,\"type\":1,\"step\":10,\"sp\":400,\"maxSp\":400,\"minSp\":150,\"aux\":null}},\"outputs\":{\"out0\":{\"id\":0,\"on\":false,\"onAut\":true},\"out1\":{\"id\":10,\"on\":false,\"onAut\":true},\"out2\":{\"id\":11,\"on\":false,\"onAut\":true},\"out3\":{\"id\":12,\"on\":false,\"onAut\":true},\"out4\":null}}}","payloadType":"json","x":340,"y":160,"wires":[["ad9ef73ee13727ca"]]},{"id":"498b8188ed61064e","type":"tcp request","z":"fbe3de3d88fb5cbe","name":"","server":"","port":"","out":"time","ret":"string","splitc":"0","newline":"","trim":false,"tls":"","x":570,"y":100,"wires":[["cb1668e042924bef","ad9ef73ee13727ca"]]},{"id":"cb1668e042924bef","type":"debug","z":"fbe3de3d88fb5cbe","name":"debug 41","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":760,"y":40,"wires":[]},{"id":"ad9ef73ee13727ca","type":"function","z":"fbe3de3d88fb5cbe","name":"Processamento da resposta","func":"var erroCode = msg.payload.response?.error ?? null; // Captura o código do erro (int)\nvar erro = `Erro desconhecido \"${erroCode}\"`; // Mensagem padrão se não houver correspondência\n\nconst text = {\n  0: ['Sem erro ou aviso.', 'Nenhuma descrição.'],\n  1: ['Sobreaquecimento.', 'O sensor de temperatura do coletor solar identificou um valor elevado. Para evitar qualquer dano à tubulação, a circulação de água será evitada.'],\n  2: ['Anticongelamento.', 'O sensor de temperatura do coletor solar identificou um valor muito baixo. Para evitar qualquer dano à tubulação, a circulação de água será ativada.'],\n  3: ['Falta de Água.', 'O sensor de nível do dispositivo identificou que não há água na banheira. Por segurança, nenhuma hidromassagem pode ser acionada. Verifique se o equipamento está calibrado.'],\n  4: ['Resfriamento da Resistência.', 'A hidromassagem permanecerá ligada por mais alguns segundos, até que a resistência esteja resfriada. Não desligue a saída durante esse período.'],\n  5: ['Erro de temperatura.', 'O sensor de temperatura do dispositivo identificou uma leitura inadequada. Verifique o local do sensor e sua instalação.'],\n  6: ['Termostato Desarmado.', 'O termostato identificou um sobreaquecimento. Para evitar qualquer dano ao sistema, o aquecimento da água foi desativado.'],\n  7: ['PARÂMETRO LEGADO.', 'ESPAÇO DISPONÍVEL PARA PRODUTOS DA VERSÃO MODBUS 1.0.'],\n  8: ['Pressão Alta.', 'O calor do sistema não está sendo absorvido pela água, gerando superaquecimento. Verifique a vazão de água do sistema.'],\n  9: ['Pressão Baixa.', 'Foi identificada uma baixa pressão no gás refrigerante. Isso pode ocorrer após o primeiro acionamento da bomba ou por uma baixa temperatura ambiente.'],\n  10: ['Baixo fluxo de água.', 'Verifique a limpeza do sistema filtrante, a bomba e a posição dos registros de água do equipamento.'],\n  11: ['Ciclo de degelo.', 'O ciclo de degelo foi ativado devido à identificação de baixa temperatura ambiente.'],\n  12: ['Erro de Alimentação.', 'A alimentação do trocador se encontra fora da faixa de operação.'],\n  13: ['Parâmetro Inconsistente.', 'A configuração dos parâmetros do seu controlador apresenta uma inconsistência quanto ao setpoint. Ajuste o parâmetro de setpoint máximo do seu produto.'],\n  14: ['Erro sensor 1.', 'O sensor de temperatura T1 do dispositivo identificou uma leitura inadequada. Verifique o local do sensor e sua instalação.'],\n  15: ['Erro sensor 2.', 'O sensor de temperatura T2 do dispositivo identificou uma leitura inadequada. Verifique o local do sensor e sua instalação.'],\n  16: ['Erro sensor 3.', 'O sensor de temperatura T3 do dispositivo identificou uma leitura inadequada. Verifique o local do sensor e sua instalação.'],\n  17: ['Erro sensor 4.', 'O sensor de temperatura T4 do dispositivo identificou uma leitura inadequada. Verifique o local do sensor e sua instalação.'],\n  18: ['Erro sensor 5.', 'O sensor de temperatura T5 do dispositivo identificou uma leitura inadequada. Verifique o local do sensor e sua instalação.'],\n  19: ['Erro sensor 6.', 'O sensor de temperatura T6 do dispositivo identificou uma leitura inadequada. Verifique o local do sensor e sua instalação.'],\n  20: ['Dados inconsistentes.', 'O produto apresentou um comportamento inesperado. Entre em contato com o suporte.']\n};\n\n// Determina os valores a serem usados\nconst result = text[erroCode] ?? [erro, ''];\n\n// Inicializa msg.response caso não exista\nmsg.response = msg.response || {};\nmsg.response.error = result[0];\nmsg.response.error_description = result[1];\n\n// Processa os aquecimentos da resposta\nif (msg.payload.response?.heatings) {\n  msg.response.heatings = {};\n\n  // Itera sobre cada aquecimento e processa\n  for (let key in msg.payload.response.heatings) {\n    let heating = msg.payload.response.heatings[key];\n    \n    // Verifica se o aquecimento não é null\n    if (heating !== null) {\n      let heatingType = '';\n      \n      // Determina o tipo de aquecimento\n      switch (heating.type) {\n        case 0:\n          heatingType = 'Solar Piscina';\n          break;\n        case 1:\n          heatingType = 'Trocador de Calor Piscina';\n          break;\n        case 2:\n          heatingType = 'Elétrico Piscina';\n          break;\n        case 3:\n          heatingType = 'Solar Residencial';\n          break;\n        case 4:\n          heatingType = 'Apoio a Gás';\n          break;\n        case 5:\n          heatingType = 'Apoio Elétrico';\n          break;\n        case 6:\n          heatingType = 'Recirculação de Barrilete';\n          break;\n        case 7:\n          heatingType = 'Trocador de Calor Fairland';\n          break;\n        case 8:\n          heatingType = 'Aquecimento';\n          break;\n        case 9:\n          heatingType = 'Refrigeração';\n          break;\n        case 10:\n          heatingType = 'Termostato';\n          break;\n        default:\n          heatingType = 'Desconhecido';\n          break;\n      }\n\n      let opMode = '';\n      // Determina o modo de operação\n      switch (heating.opMode) {\n        case 0:\n          opMode = 'Desligado';\n          break;\n        case 1:\n          opMode = 'Ligado';\n          break;\n        case 2:\n          opMode = 'Automático';\n          break;\n        case 3:\n          opMode = 'Econômico';\n          break;\n        case 4:\n          opMode = 'Aquecer';\n          break;\n        case 5:\n          opMode = 'Resfriar';\n          break;\n        default:\n          opMode = 'Desconhecido';\n          break;\n      }\n\n      // Converte step, sp, minSp e maxSp para float\n      const step = heating.step / 10;\n      const minSp = heating.minSp / 10;\n      const maxSp = heating.maxSp / 10;\n      const sp = heating.sp / 10;\n\n      // Adiciona as informações no objeto msg.response.heatings\n      msg.response.heatings[key] = {\n        type: heatingType,\n        step: step,\n        minSp: minSp,\n        maxSp: maxSp,\n        sp: sp,\n        on: heating.on,\n        onAut: heating.onAut,\n        aux: heating.aux,\n        opMode: opMode\n      };\n\n      // Processa os sensores t1, t2, t3, t4, etc. e os adiciona diretamente no aquecimento\n      let sensorIndex = 1;\n      \n      // Para cada sensor, processa e converte para float\n      while (heating[`t${sensorIndex}`] !== undefined) {\n        msg.response.heatings[key][`t${sensorIndex}`] = heating[`t${sensorIndex}`] / 10;\n        sensorIndex++;\n      }\n    }\n  }\n}\n\n// Processa os outputs da resposta\nif (msg.payload.response?.outputs) {\n  msg.response.outputs = {};\n\n  // Itera sobre cada saída e processa\n  for (let key in msg.payload.response.outputs) {\n    let output = msg.payload.response.outputs[key];\n\n    // Verifica se a saída não é null\n    if (output !== null) {\n      let outputType = '';\n\n      // Determina o tipo de saída\n      switch (output.id) {\n        case 0:\n          outputType = 'Filtro';\n          break;\n        case 1:\n          outputType = 'Apoio Elétrico';\n          break;\n        case 2:\n          outputType = 'Apoio a Gás';\n          break;\n        case 3:\n          outputType = 'Recirculação';\n          break;\n        case 4:\n          outputType = 'Borbulhador';\n          break;\n        case 5:\n          outputType = 'Circulação';\n          break;\n        case 10:\n        case 11:\n        case 12:\n        case 13:\n        case 14:\n        case 15:\n        case 16:\n        case 17:\n        case 18:\n        case 19:\n          outputType = 'Auxiliares';\n          break;\n        case 20:\n        case 21:\n        case 22:\n        case 23:\n        case 24:\n        case 25:\n        case 26:\n        case 27:\n        case 28:\n        case 29:\n          outputType = 'Cascatas';\n          break;\n        case 30:\n        case 31:\n        case 32:\n        case 33:\n        case 34:\n        case 35:\n        case 36:\n        case 37:\n        case 38:\n        case 39:\n          outputType = 'Hidros';\n          break;\n        case 40:\n        case 41:\n        case 42:\n        case 43:\n        case 44:\n        case 45:\n        case 46:\n        case 47:\n        case 48:\n        case 49:\n        case 50:\n        case 51:\n        case 52:\n        case 53:\n        case 54:\n        case 55:\n          outputType = 'Interruptores';\n          break;\n        default:\n          outputType = 'Desconhecido';\n          break;\n      }\n\n      // Adiciona as informações no objeto msg.response.outputs\n      msg.response.outputs[key] = {\n        type: outputType,\n        on: output.on,\n        onAut: output.onAut\n      };\n    }\n  }\n}\n\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":820,"y":100,"wires":[["62f4accb9cfa37fe"]]},{"id":"62f4accb9cfa37fe","type":"debug","z":"fbe3de3d88fb5cbe","name":"debug 43","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1040,"y":100,"wires":[]},{"id":"1904d1e0ef40850c","type":"inject","z":"fbe3de3d88fb5cbe","name":"Simular resposta","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"command\":\"getDevice\",\"response\":{\"id\":11462,\"reset\":0,\"error\":0,\"updating\":false,\"firmware\":\"P858V2\",\"firmware2\":\"P844V0\",\"timezone\":-10800,\"leds\":{\"led0\":{\"on\":false,\"onAut\":false,\"effect\":255,\"brightness\":48,\"speed\":50,\"type\":2,\"saturation\":66,\"color\":[163,255,40]}},\"heatings\":{\"heat0\":{\"on\":false,\"onAut\":true,\"opMode\":0,\"type\":1,\"step\":10,\"sp\":400,\"maxSp\":400,\"minSp\":150,\"aux\":null},\"heat1\":{\"on\":true,\"onAut\":true,\"opMode\":2,\"type\":1,\"step\":10,\"sp\":400,\"maxSp\":400,\"minSp\":150,\"aux\":null}},\"outputs\":{\"out0\":{\"id\":0,\"on\":false,\"onAut\":true},\"out1\":{\"id\":10,\"on\":false,\"onAut\":true},\"out2\":{\"id\":11,\"on\":false,\"onAut\":true},\"out3\":{\"id\":12,\"on\":false,\"onAut\":true},\"out4\":{\"id\":12,\"on\":false,\"onAut\":true}}}}","payloadType":"json","x":340,"y":260,"wires":[["ad9ef73ee13727ca"]]}]
1 curtida