Contexte: comme l'un de mes hobbies, j'avais une «Smart Home». Je veux de beaux appareils, mais je veux aussi la liberté et l'intimité. Par conséquent, je suis engagé dans le croisement de Xiaomi uzhik avec l'hérisson
Home Assistant .
Pour maintenir un environnement confortable, nous devons savoir ce qui se passe à la maison. En bref, des capteurs sont nécessaires. Xiaomi en a de nombreux différents, mais surtout j'ai aimé le thermomètre carré à encre électronique. Mais il n'est pas du tout intelligent dans le sens où il ne fournit aucune interface, à l'exception de l'interface graphique - ni WiFi, ni BLE, ni ZigBee. Mais les piles CR2032 durent plusieurs années. Il existe également une version avec Bluetooth, mais elle est un peu moins élégante - une sorte de crêpe épaisse.
Et au début du printemps, un nouveau capteur de température / humidité a été annoncé, sur encre électronique, avec BLE, et même avec horloge. Je n'ai pas vraiment besoin d'une montre, mais tout le reste a immédiatement supprimé tous les arguments rationnels et le thermomètre a été commandé dans l'une des boutiques en ligne populaires, en pré-commande. Il est monté, il est monté et est finalement arrivé.

Le capteur a été ajouté sans problème à l'application MiHome (j'ai une interface anglaise partout, avec la version russe de MiHome, disent-ils, il y avait des difficultés de traduction). Affiche les valeurs actuelles et l'historique des modifications des lectures.
Mais avec l'intégration dans le Home Assistant, des difficultés se sont produites. Le composant existant pour le capteur de température ne voulait en aucun cas prendre des données de l'appareil et s'est plaint du format de données incorrect. Eh bien, il n'y a rien à faire, nous sortons une pelle et commençons à creuser.
La première pensée a été de se familiariser avec le dispositif du protocole BLE, mais après avoir évalué la taille de la documentation, il a été décidé de passer à la méthode de poke populaire.
La première approche de la coque
Pour commencer, ouvrez le terminal sur ubuntu et exécutez bluetoothctl. Nous voyons ce qui suit:
[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 est un ancien capteur de température, LYWSD02 est un nouveau. La différence dans le format de dénomination du modèle est quelque peu alarmante.
Ensuite, vous devez en quelque sorte lire, et quel type de données en général peut être obtenu auprès de nous. Il a ouvert les sources de la bibliothèque
mitemp , qui est utilisée dans le Home Assistant pour recevoir les données de l'ancien capteur. Là, j'ai trouvé que la bibliothèque blewrap est utilisée, qui, à son tour, est un wrapper sur deux bibliothèques Python pour travailler avec BLE. Je n'ai pas besoin d'autant de couches, nous utiliserons
bluepy . Il y a de la documentation, ce n'est pas beaucoup et pas peu, nous lisons et écrivons un script qui passe par tous les champs de données qui se trouvent sur l'appareil.
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 général, tout est simple - un appareil BLE fournit un ensemble de services, chacun consistant en un ensemble de caractéristiques. Chaque caractéristique peut être l'un des 8 types, pour une caractéristique, vous pouvez spécifier plusieurs types en même temps. Les services et fonctionnalités sont identifiés de deux manières: une adresse sous la forme d'une valeur HEX et d'un UUID. Je suis plus familier avec le travail avec UUID.
J'ai donc examiné toutes les spécifications des deux capteurs, les ai regardées et j'ai réalisé que, encore une fois, des appareils de fabricants complètement différents sont vendus sous la marque Xiaomi. Parmi les valeurs de l'ancien capteur, «Cleargrass Inc» a été trouvé, et dans le nouveau, «miaomiaoce.com». La structure des services et les caractéristiques de ces deux capteurs sont également complètement différentes, et la liste des caractéristiques du nouveau capteur est deux fois plus longue. Ensuite, il est devenu clair que vous devez écrire votre propre bibliothèque pour l'intégration avec le capteur (non, bien sûr, j'ai googlé au début, peut-être qu'il y a quelque chose d'utile sur demande LYWSD02, mais je n'ai rien donné de sensible à Google).
Alors, comment obtenez-vous les données?
Parmi les types de caractéristiques disponibles, outre READ, il existe également WRITE et NOTIFY. WRITE - pour envoyer des données à l'appareil et NOTIFY - pour recevoir des données. Il y a également WRITE NOTIFY en même temps - l'appareil n'enverra des données qu'après avoir été abonné en envoyant l'octet souhaité avec la commande WRITE.
Les tentatives de faire quelque chose avec mes mains n'ont donné aucun résultat, la première ligne de désespoir a été atteinte, mais à ce moment-là, j'ai lu des articles sur l'artisanat basé sur des puces de Nordic Semiconductors et
j'ai mis le programme
nRF Connect sur mon smartphone. Avec son aide, j'ai pu m'abonner à tous les services fournis par l'appareil, j'ai enregistré les journaux de réponses et j'ai commencé à essayer de comprendre ce qui s'y trouve.

Ces triples flèches activent l'abonnement.
La particularité de l'ancien capteur était que les données sur la température et l'humidité se présentaient sous la forme d'une chaîne UTF, tandis que la nouvelle renvoyait tout sous forme binaire.
Abonnez-vous aux notifications
Pour recevoir des données du capteur, vous devez envoyer une demande d'abonnement. Dans la bibliothèque mitemp, deux octets pour la caractéristique ont été envoyés pour cela, mais on ne sait pas où l'obtenir. Ici, j'ai regardé la structure de données pour l'ancien capteur dans nRF Connect et j'ai remarqué que l'adresse souhaitée est spécifiée pour la caractéristique avec des données, comme un descripteur. Puis j'ai recommencé à lire la documentation de bluepy et j'ai réalisé que l'adresse du descripteur peut être facilement obtenue à partir de l'objet caractéristiques. Il ne reste plus qu'à écrire une classe avec une méthode de rappel, qui recevra les données de la notification.
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'
Nous séparons les grains de l'ivraie
Heureusement, seules trois caractéristiques ont été marquées comme WRITE NOTIFY, tandis que les données étaient fournies avec des fréquences différentes et, euh ..., des caractéristiques visuelles.
La première demande a envoyé immédiatement un grand tapis de données, puis est restée bloquée. Dans ce cas, le premier octet était un nombre croissant de façon monotone. Cela semble être une histoire accumulée de moyennes.
Le deuxième et le troisième ont été envoyés périodiquement, mais en y regardant de près, j'ai vu que l'un d'eux ne change pas, et dans les données du deuxième un seul octet est changé. Eh bien, c'est l'heure actuelle (je vous rappelle que ce thermomètre a une horloge. Il devrait y avoir une horloge dans tout appareil qui se respecte pour une maison intelligente).
Supposons que la troisième caractéristique soit des données utiles sur la température et l'humidité. Pour confirmer l'hypothèse, une expérience physique a été menée - il est allé vers le capteur et a respiré grossièrement dessus. Les valeurs des données ont fortement augmenté à l'écran et les octets ont changé dans le terminal. Hourra, les données sont quelque part à proximité.
Analyse des données
Je travaille généralement avec des données texte (obtenir des données HTTP sous forme de JSON / xml, les mettre dans un fichier ou dans une base de données), donc je ne comprenais pas vraiment comment aborder la tâche. Par conséquent, j'ai commencé à essayer de transformer les données de différentes manières qui peuvent être faites à partir de python. J'ai écrit ici une telle fonction de conversion et j'ai commencé à voir comment cela corrélait avec les données sur l'écran du capteur.
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')))
Des lignes de divers degrés d'obscurité se déversèrent dans la console, mais le troisième octet était toujours un nombre, et ce nombre coïncidait avec la valeur d'humidité. Par souci de fidélité, j'ai encore une fois respiré le capteur - et les valeurs d'humidité à l'écran et dans le troisième octet ont changé de la même manière!
Ensuite, j'ai suggéré que la température soit stockée dans les deux premiers octets. Pour que les données changent, j'ai transféré le capteur sur un sèche-serviettes dans la salle de bain. Mais peu importe comment j'ai essayé de transformer les résultats, les nombres requis n'ont pas fonctionné.
Sur la voie du succès
À ce moment, j'ai regardé à nouveau la
description du capteur et j'ai vu qu'il y avait un capteur de Swiss Sensirion à l'intérieur. Cela vaut probablement la peine de commencer par cela, mais ce n'est pas notre méthode. Un tas de capteurs ont été trouvés sur le site Web de Swiss Sensirion, et des fiches techniques pour eux. Dans la fiche technique, entre autres choses, une formule a été trouvée pour convertir les octets transmis sur le bus I2C en un nombre.
Mais ... Il s'est avéré des valeurs très étranges. Quelque chose comme -34,66, mais j'étais clairement plus chaud. De tristesse et de tristesse, j'ai même ouvert le capteur et vérifié si le capteur de Swiss Sensirion était vrai là-bas. Il s'est avéré que c'était vrai, mais avec l'indice SHTC3, et il lui fallait une formule légèrement différente.
Cependant, les données après la conversion ne ressemblaient même pas étroitement aux vraies. Ici, j'étais encore plus triste, j'ai ouvert le code source de la bibliothèque pour SHTC3 d'Adfruit et j'ai commencé à essayer d'adapter le code de transformation de C ++ en python. J'ai tout apporté sur la tablette - données brutes, structure convertie et résultat.
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')
Vous avez quelque chose comme ça:
b',\n2' 2604 -44.130821228027344 b'-\n2' 2605 -44.1304874420166 b'+\n2' 2603 -44.131155014038086 b',\n2' 2604 -44.130821228027344
Oui ... il fait un peu froid ... Mais, attendez, attendez, qu'est-ce que 2604? Ça y est, 26,0 degrés à l'écran! Pour confirmer l'hypothèse, il a de nouveau amené le capteur à la batterie, vérifié que les valeurs coïncident.
En conséquence, nous obtenons le code de conversion de données suivant:
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)
Épilogue
L'opération de connexion au capteur et de recherche de l'algorithme de transformation correct a pris quelques soirées. Plusieurs fois, j'ai voulu tout laisser tomber, mais en même temps de nouvelles idées sont venues et j'ai continué à essayer.
Maintenant, les données sont transférées vers le Home Assistant, puis vous devez terminer le code d'intégration et, éventuellement, le réécrire de bluepy à bleak, car bleak utilise async / wait et est mieux adapté pour le Home Assistant écrit par aiohttp.

Références: