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'
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.
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.
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: