Como tirei dados de um termômetro BLE da Xiaomi

Antecedentes: como um dos meus hobbies, eu tinha uma “Casa Inteligente”. Quero dispositivos bonitos, mas também quero liberdade e privacidade. Portanto, estou envolvido no cruzamento do Xiaomi uzhik com o ouriço doméstico Assistente .

Para manter um ambiente confortável, precisamos saber o que está acontecendo em casa. Em resumo, são necessários sensores. A Xiaomi tem muitos diferentes, mas acima de tudo, gostei do termômetro quadrado em tinta eletrônica. Mas ele não é nada inteligente no sentido de que ele não fornece nenhuma interface, exceto a gráfica - nem WiFi, nem BLE, nem ZigBee. Mas as baterias CR2032 duram vários anos. Há também uma versão com bluetooth, mas é um pouco menos elegante - uma espécie de panqueca espessa.

E no início da primavera, um novo sensor de temperatura / umidade foi anunciado, em tinta eletrônica, com BLE e até com um relógio. Eu realmente não preciso de um relógio, mas todo o resto imediatamente suprimiu todos os argumentos racionais e o termômetro foi encomendado em uma das lojas on-line populares, em pré-encomenda. Ele cavalgou, cavalgou e finalmente chegou.



O sensor foi adicionado ao aplicativo MiHome sem problemas (eu tenho uma interface em inglês em todos os lugares, com a versão em russo do MiHome, dizem eles, houve dificuldades de tradução). Mostra os valores atuais e o histórico de alterações nas leituras.

Mas com a integração no Assistente Doméstico, ocorreram dificuldades. O componente existente para o sensor de temperatura de forma alguma desejava coletar dados do dispositivo e reclamou do formato de dados incorreto. Bem, não há nada a fazer, pegamos uma pá e começamos a cavar.

O primeiro pensamento foi familiarizar-se com o dispositivo do protocolo BLE, mas depois de avaliar o tamanho da documentação, decidiu-se mudar para o método de puxão popular.

A primeira abordagem para o shell


Para começar, abra o terminal no ubuntu e execute bluetoothctl. Vemos o seguinte:

[NEW] Controller 00:1A:7D:DA:71:13 fett [default] [NEW] Device 3F:59:C8:80:70:BE LYWSD02 [NEW] Device 4C:65:A8:DC:0D:AF MJ_HT_V1 

MJ_HT_V1 é um sensor de temperatura antigo, LYWSD02 é um novo. A diferença no formato de nomeação do modelo é um tanto alarmante.

Então você precisa ler de alguma forma e que tipo de dados em geral pode ser obtido conosco. Ele abriu as fontes da biblioteca mitemp , que é usada no Home Assistant para receber dados do sensor antigo. Aí descobri que a biblioteca blewrap é usada, que, por sua vez, é um invólucro em duas bibliotecas Python para trabalhar com o BLE. Não preciso de tantas camadas, usaremos o bluepy . Existe documentação, não é muito e nem um pouco, lemos e escrevemos um script que passa por todos os campos de dados que estão no dispositivo.

 from bluepy import btle mac = '3F:59:C8:80:70:BE' p = btle.Peripheral(mac) for s in p.getServices(): print('Service:', s.uuid) for c in s.getCharacteristics(): print('\tCharacteristic:', c.uuid) print('\t\t', c.propertiesToString()) if c.supportsRead(): print('\t\t', c.read()) 

Em geral, tudo é simples - um dispositivo BLE fornece um conjunto de serviços, cada um dos quais consiste em um conjunto de características. Cada característica pode ser um dos 8 tipos; para uma característica, você pode especificar vários tipos ao mesmo tempo. Os serviços e recursos são identificados de duas maneiras - um endereço na forma de um valor HEX e um UUID. Estou mais familiarizado com o trabalho com o UUID.

Então, considerei todas as especificações dos dois sensores, olhei para eles e percebi que novamente dispositivos de fabricantes completamente diferentes são vendidos sob a marca Xiaomi. Entre os valores do sensor antigo, foi encontrado "Cleargrass Inc" e, no novo, "miaomiaoce.com". A estrutura dos serviços e as características desses dois sensores também é completamente diferente, e a lista de características do novo sensor é duas vezes maior. Então ficou claro que você precisa escrever sua própria biblioteca para integração com o sensor (não, é claro que eu pesquisei no início, talvez exista algo útil a pedido do LYWSD02, mas não dei nada sensato ao google).

Então, como você obtém os dados?


Entre os tipos de características disponíveis, além de READ, há também WRITE e NOTIFY. WRITE - para enviar dados ao dispositivo e NOTIFY - para receber dados. Também há WRITE NOTIFY ao mesmo tempo - o dispositivo só envia dados após a assinatura, enviando o byte desejado com o comando WRITE.

Tentativas de fazer algo com as mãos não deram resultado, a primeira linha de desespero foi alcançada, mas naquele momento eu li artigos sobre artesanato baseados em chips da Nordic Semiconductors e coloquei o programa nRF Connect no meu smartphone. Com sua ajuda, consegui assinar todos os serviços que o dispositivo fornecia, salvou os registros de respostas e comecei a tentar entender o que havia neles.



Essas setas triplas ativam a assinatura.

Uma característica do sensor antigo era que os dados de temperatura e umidade vinham na forma de uma string UTF, enquanto o novo retornava tudo na forma binária.

Inscrever-se em Notificações


Para receber dados do sensor, você precisa enviar uma solicitação de assinatura. Na biblioteca mitemp, foram enviados dois bytes para a característica, mas não está claro onde obtê-la.Aqui, olhei para a estrutura de dados do sensor antigo no nRF Connect e notei que o endereço desejado é especificado para a característica com dados, como um descritor. Então comecei a ler a documentação do bluepy novamente e percebi que o endereço do descritor pode ser facilmente obtido a partir do objeto de características. Resta apenas escrever uma classe com um método de retorno de chamada, que receberá dados da notificação.

 class MyDelegate(btle.DefaultDelegate): def handleNotification(self, cHandle, data): print(data) mac_addr = '3F:59:C8:80:70:BE' p = btle.Peripheral(mac_addr) p.setDelegate(MyDelegate()) uuid = 'EBE0CCC1-7A0A-4B0C-8A1A-6FF2997DA3A6' #    ,        ch = p.getCharacteristics(uuid=uuid)[0] #     desc = ch.getDescriptors(forUUID=0x2902)[0] #  ,         desc.write(0x01.to_bytes(2, byteorder="little"), withResponse=True) while True: p.waitForNotifications(5.0) 

Separamos os grãos do joio


Felizmente, apenas três características foram marcadas como WRITE NOTIFY, enquanto os dados vieram com diferentes frequências e, um ..., recursos visuais.

A primeira solicitação enviou imediatamente uma grande quantidade de dados e depois parou. Nesse caso, o primeiro byte era um número monotonamente crescente. Este parece ser um histórico acumulado de médias.

O segundo e o terceiro foram enviados periodicamente, mas olhando atentamente, vi que um deles não muda e, nos dados do segundo, apenas um byte é alterado. Bem, então é a hora atual (lembro que este termômetro tem um relógio. Deve haver um relógio em qualquer dispositivo que se preze para uma casa inteligente).

Suponha que a terceira característica seja dados úteis sobre temperatura e umidade. Para confirmar a hipótese, foi realizado um experimento físico - ele foi até o sensor e respirou com dificuldade. Os valores dos dados aumentaram bastante no visor e os bytes foram alterados no terminal. Hooray, os dados estão em algum lugar próximo.

Análise de dados


Eu costumo trabalhar com dados de texto (obter dados HTTP na forma de JSON / xml, colocá-los em um arquivo ou no banco de dados), por isso não entendi realmente como abordar a tarefa. Portanto, comecei a tentar transformar os dados de diferentes maneiras que podem ser feitas a partir de python. Escrevi aqui uma função de conversão e comecei a observar como isso se correlaciona com os dados na tela do sensor.

 def parse(v): print([x for x in v]) print('{0:#x}'.format(int.from_bytes(data, byteorder='big'))) print('{0:#x}'.format(int.from_bytes(data, byteorder='little'))) 

Linhas de diferentes graus de obscuridade caíam no console, mas o terceiro byte sempre era um número, e esse número coincidia com o valor da umidade. Por uma questão de fidelidade, respirei novamente o sensor - e os valores de umidade na tela e no terceiro byte mudaram da mesma forma!

Então sugeri que a temperatura fosse armazenada nos dois primeiros bytes. Para que os dados mudassem, transferi o sensor para um toalheiro aquecido no banheiro. Mas não importa como eu tentei transformar os resultados, os números necessários não deram certo.

No caminho para o sucesso


Naquele momento, olhei novamente para a descrição do sensor e vi que havia um sensor da Swiss Sensirion dentro. Provavelmente vale a pena começar com isso, mas esse não é o nosso método. Vários sensores foram encontrados no site da Swiss Sensirion e folhas de dados para eles. Na folha de dados, entre outras coisas, foi encontrada uma fórmula para converter bytes transmitidos pelo barramento I2C em um número.

T[°C]=45+175 cdot fracST2161



Mas ... Aconteceu valores muito estranhos. Algo como -34,66, mas eu estava claramente mais quente. De tristeza e tristeza, eu até abri o sensor e verifiquei se o sensor da Swiss Sensirion era verdadeiro lá. Aconteceu que era verdade, mas com o índice SHTC3, e precisava de uma fórmula um pouco diferente para isso.

T[°C]=45+175 cdot fracST216



No entanto, os dados após a conversão nem se assemelham aos reais. Aqui fiquei ainda mais triste, abri o código fonte da biblioteca para o SHTC3 da Adfruit e comecei a tentar adaptar o código de transformação de C ++ para python. Trouxe tudo para o tablet - dados brutos, estrutura e resultado convertidos.

 def handleNotification(self, cHandle, data): temp = data[:2] humid = data[2] unpacked = struct.unpack('H', temp)[0] print(data, unpacked, -45 + 175 * unpacked / 2 ** 19, sep='\t') 

Tem algo parecido com isto:

 b',\n2' 2604 -44.130821228027344 b'-\n2' 2605 -44.1304874420166 b'+\n2' 2603 -44.131155014038086 b',\n2' 2604 -44.130821228027344 

Sim ... está meio frio ... Mas, espere, espere, o que é 2604? É isso, 26,0 graus na tela! Para confirmar a hipótese, ele novamente levou o sensor à bateria, verificando se os valores coincidem.

Como resultado, obtemos o seguinte código de conversão de dados:

 def handleNotification(self, cHandle, data): humid_bytes = data[2] temp_bytes = data[:2] humidity = humid_bytes temperature = struct.unpack('H', temp_bytes)[0] / 100 print(temperature, humidity) 

Epílogo


A operação para conectar-se ao sensor e procurar o algoritmo de transformação correto levou algumas noites. Várias vezes eu quis largar tudo, mas ao mesmo tempo novas idéias surgiram e continuei a tentar.

Agora que os dados são transferidos para o Home Assistant, você precisa finalizar o código de integração e, possivelmente, reescrevê-lo de bluepy para bleak, pois o bleak usa async / waitit e é mais adequado para o Home Assistant escrito por aiohttp.



Referências:


Source: https://habr.com/ru/post/pt452558/


All Articles