¿Cómo tomé datos de un termómetro BLE de Xiaomi?

Antecedentes: como uno de mis pasatiempos, tenía una "casa inteligente". Quiero dispositivos hermosos, pero también quiero libertad y privacidad. Por lo tanto, estoy comprometido en el cruce Xiaomi uzhik con el Asistente de inicio de hedgehog.

Para mantener un ambiente confortable, necesitamos saber qué sucede en casa. En resumen, se necesitan sensores. Xiaomi tiene muchos diferentes, pero sobre todo me gustó el termómetro cuadrado en tinta electrónica. Pero no es nada inteligente en el sentido de que no proporciona ninguna interfaz, excepto la gráfica, ni WiFi, ni BLE, ni ZigBee. Pero las baterías CR2032 duran varios años. También hay una versión con bluetooth, pero es un poco menos elegante, una especie de panqueque grueso.

Y a principios de la primavera, se anunció un nuevo sensor de temperatura / humedad, en tinta electrónica, con BLE e incluso con un reloj. Realmente no necesito un reloj, pero todo lo demás suprimió de inmediato todos los argumentos racionales y el termómetro se ordenó en una de las tiendas en línea populares, por pedido anticipado. Cabalgó, cabalgó y finalmente llegó.



El sensor se agregó a la aplicación MiHome sin problemas (tengo una interfaz en inglés en todas partes, con la versión rusa de MiHome, dicen, hubo dificultades de traducción). Muestra los valores actuales y el historial de cambios en las lecturas.

Pero con la integración en el Home Assistant se han producido dificultades. El componente existente para el sensor de temperatura de ninguna manera quiso tomar datos del dispositivo y se quejó del formato de datos incorrecto. Bueno, no hay nada que hacer, sacamos una pala y comenzamos a cavar.

El primer pensamiento fue familiarizarse con el dispositivo del protocolo BLE, pero después de evaluar el tamaño de la documentación, se decidió cambiar al popular método de empuje.

El primer acercamiento al caparazón


Para comenzar, abra la terminal en ubuntu y ejecute bluetoothctl. Vemos lo siguiente:

[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 es un sensor de temperatura antiguo, LYWSD02 es uno nuevo. La diferencia en el formato de nombre del modelo es algo alarmante.

Entonces necesita leer de alguna manera, y qué tipo de datos en general se pueden obtener de nosotros. Abrió las fuentes de la biblioteca mitemp , que se utiliza en el Asistente de inicio para recibir datos del sensor anterior. Allí descubrí que se usa la biblioteca blewrap, que, a su vez, es un contenedor en dos bibliotecas de Python para trabajar con BLE. No necesito tantas capas, usaremos bluepy . Hay documentación, no es mucho ni poco, leemos y escribimos un script que pasa por todos los campos de datos que están en el 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()) 

En general, todo es simple: un dispositivo BLE proporciona un conjunto de servicios, cada uno de los cuales consiste en un conjunto de características. Cada característica puede ser uno de los 8 tipos, para una característica puede especificar varios tipos al mismo tiempo. Los servicios y características se identifican de dos maneras: una dirección en forma de un valor HEX y un UUID. Estoy más familiarizado con trabajar con UUID.

Entonces, consideré todas las especificaciones para ambos sensores, las miré y me di cuenta de que nuevamente los dispositivos de fabricantes completamente diferentes se venden bajo la marca Xiaomi. Entre los valores del antiguo sensor, se encontró "Cleargrass Inc", y en el nuevo, "miaomiaoce.com". La estructura de servicios y características de estos dos sensores también es completamente diferente, y la lista de características del nuevo sensor es el doble. Luego quedó claro que necesita escribir su propia biblioteca para la integración con el sensor (no, por supuesto que busqué en Google al principio, tal vez hay algo útil a pedido LYWSD02, pero no le di a Google nada sensato).

Entonces, ¿cómo se obtienen los datos?


Entre los tipos de características disponibles, además de LEER, también hay ESCRIBIR y NOTIFICAR. ESCRIBIR - para enviar datos al dispositivo, y NOTIFICAR - para recibir datos. También hay WRITE NOTIFY al mismo tiempo: el dispositivo solo enviará datos después de suscribirse enviando el byte deseado con el comando WRITE.

Los intentos de hacer algo con mis manos no dieron ningún resultado, se llegó a la primera línea de desesperación, pero en ese momento leí artículos sobre artesanías basadas en chips de Nordic Semiconductors y puse el programa nRF Connect en mi teléfono inteligente. Con su ayuda, pude suscribirme a todos los servicios que proporcionaba el dispositivo, guardé los registros de respuestas y comencé a tratar de comprender qué hay en ellos.



Estas flechas triples activan la suscripción.

La peculiaridad del antiguo sensor era que los datos sobre temperatura y humedad se presentaban en forma de una cadena UTF, mientras que el nuevo devolvía todo en forma binaria.

Suscríbase a las notificaciones


Para recibir datos del sensor, debe enviar una solicitud de suscripción. En la biblioteca mitemp, se enviaron dos bytes para la característica para esto, pero no está claro dónde obtenerla. Aquí miré cómo se ve la estructura de datos para el sensor anterior en nRF Connect y noté que la dirección deseada se especifica para la característica con datos, como un descriptor. Luego comencé a leer la documentación para bluepy nuevamente y me di cuenta de que la dirección del descriptor se puede obtener fácilmente del objeto de características. Solo queda escribir una clase con un método de devolución de llamada, que recibirá datos de la notificación.

 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 los granos de la paja


Afortunadamente, solo tres características se marcaron como ESCRIBIR NOTIFICACIÓN, mientras que los datos llegaron con diferentes frecuencias y, um ..., características visuales.

La primera solicitud envió inmediatamente una gran superficie de datos y luego se atascó. En este caso, el primer byte fue un número monótonamente creciente. Esto parece ser una historia acumulada de promedios.

El segundo y el tercero se enviaron periódicamente, pero mirando de cerca, vi que uno de ellos no cambia, y en los datos del segundo solo se cambia un byte. Bueno, entonces esta es la hora actual (les recuerdo que este termómetro tiene un reloj. Debería haber un reloj en cualquier dispositivo que se respete para una casa inteligente).

Supongamos que la tercera característica son datos útiles sobre temperatura y humedad. Para confirmar la hipótesis, se realizó un experimento físico: fue al sensor y respiró bruscamente. Los valores de los datos aumentaron considerablemente en la pantalla y los bytes cambiaron en el terminal. Hurra, los datos están en algún lugar cercano.

Análisis de datos


Por lo general, trabajo con datos de texto (obtengo datos HTTP en forma de JSON / xml, los pongo en un archivo o en una base de datos), por lo que realmente no entendí cómo abordar la tarea. Por lo tanto, comencé a tratar de transformar los datos de diferentes maneras que se pueden hacer desde python. Escribí aquí una función de conversión y comencé a ver cómo esto se correlaciona con los datos en la pantalla del 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'))) 

Líneas de diversos grados de oscuridad se vertieron en la consola, pero el tercer byte siempre fue un número, y este número coincidió con el valor de humedad. En aras de la fidelidad, una vez más respiré en el sensor, ¡y los valores de humedad en la pantalla y en el tercer byte cambiaron igual!

Luego sugerí que la temperatura se almacenara en los primeros dos bytes. Para que los datos cambien, transferí el sensor a un toallero calentado en el baño. Pero no importa cómo intenté transformar los resultados, los números requeridos no funcionaron.

En el camino al exito


En ese momento, volví a mirar la descripción del sensor y vi que dentro había un sensor de Swiss Sensirion. Probablemente valga la pena comenzar con esto, pero este no es nuestro método. Se encontraron un montón de sensores en el sitio web de Swiss Sensirion, y hojas de datos para ellos. En la hoja de datos, entre otras cosas, se encontró una fórmula para convertir bytes transmitidos a través del bus I2C a un número.

T[°C]=45+175 cdot fracST2161



Pero ... Resultó valores muy extraños. Algo así como -34.66, pero estaba claramente más cálido. De tristeza y tristeza, incluso abrí el sensor y verifiqué si el sensor de Swiss Sensirion era cierto allí. Resultó que era cierto, pero con el índice SHTC3, y necesitaba una fórmula ligeramente diferente.

T[°C]=45+175 cdot fracST216



Sin embargo, de todos modos, los datos después de la conversión ni siquiera se parecían mucho a los reales. Aquí estaba aún más triste, abrí el código fuente de la biblioteca para SHTC3 de Adfruit y comencé a tratar de adaptar el código de transformación de C ++ a Python. Traje todo a la tableta: datos en bruto, estructura convertida y resultado.

 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') 

Tengo algo como esto:

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

Sí ... hace un poco de frío ... Pero, espera, espera, ¿qué es 2604? ¡Esto es, 26.0 grados en la pantalla! Para confirmar la hipótesis, volvió a llevar el sensor a la batería y comprobó que los valores coinciden.

Como resultado, obtenemos el siguiente código de conversión de datos:

 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


La operación para conectarse al sensor y buscar el algoritmo de transformación correcto tomó un par de noches. Varias veces quise dejarlo todo, pero al mismo tiempo surgieron nuevas ideas y continué intentándolo.

Ahora los datos se transfieren al Asistente de inicio, entonces debe finalizar el código de integración y, posiblemente, reescribirlo de bluepy a bleak, ya que bleak usa async / wait y es más adecuado para el Asistente de inicio escrito por aiohttp.



Referencias


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


All Articles