Entrée
Il y a déjà eu
un article sur le Gicktime consacré à l'analyse du protocole de la bouilloire Redmond SkyKettle. Cependant, là, ils ont parlé du modèle RK-M171S, ici nous parlerons d'un G200S plus fonctionnel. Dans ce modèle, le protocole d'interaction a changé, à cause duquel l'approche de l'auteur de l'article précédent ne fonctionne plus, et des fonctions supplémentaires de la veilleuse et d'indication de la température actuelle en couleur sont apparues.
Dans cet article, je présenterai les résultats d'une analyse de protocole avec des exemples de code python (si quelqu'un veut développer son module / application pour contrôler la théière). À la fin de l'article se trouve également un lien vers un module prêt à l'emploi pour connecter une théière à HomeAssistant (c'est ma première expérience d'écriture en python après avoir suivi un cours en ligne, donc ce module peut et doit même être amélioré).
Tous ceux qui sont intéressés, bienvenue au chat.
Problèmes et tâches
Cette théière a un gros inconvénient (à l'exception de celles indiquées par l'auteur du premier article): dès que la théière est retirée du stand, l'heure actuelle est réinitialisée et, par conséquent, le planning ne peut pas être utilisé pour faire bouillir la théière. Selon les idées des auteurs de cette création, à chaque fois après avoir remis la bouilloire sur le stand, vous devez lancer leur application propriétaire et synchroniser la bouilloire avec un smartphone. Ainsi, au lieu de faciliter les tâches de routine, la technologie «intelligente» nous forme à effectuer des actions supplémentaires. Mais tout a changé lorsque HomeAssistant est apparu dans la maison. J'ai alors décidé de comprendre le protocole.
Les outils
Honnêtement, j'ai essayé de décompiler et d'analyser l'application d'origine, mais j'ai échoué. Les outils que j'ai utilisés ne m'ont pas permis de comprendre la logique de la bouilloire. Toutes les procédures et fonctions ont été obtenues par des «courbes», sans nom (par type a, b, c, etc.). Peut-être que je n'ai pas assez d'expérience et de compétence. Au final, j'ai suivi le même chemin que l'auteur de l'article précédent. La seule différence significative est que j'ai utilisé le mode interactif de l'utilitaire gatttool. L'avantage est que ce mode élimine toutes sortes de "races", à propos desquelles l'auteur du premier article a écrit.
Puisque HomeAssistant est écrit en python, nous y écrirons toutes les commandes supplémentaires. Pour utiliser le mode de fonctionnement interactif de gatttool en python, la bibliothèque pexpect nous aidera, vous permettant de générer l'essence des applications tierces et de surveiller leur sortie (notoirement courbée).
Pratique
J'enverrai à nouveau le premier article sur la description générale du protocole d'échange à l'auteur de l'article, donc sans plus tarder, nous procéderons aux commandes de contrôle.
- Installation et déconnexion
Établissez une connexion:
child = pexpect.spawn("gatttool -I -t random -b " + mac, ignore_sighup=False) child.expect(r'\[LE\]>', timeout=3) child.sendline("connect") child.expect(r'Connection successful.*\[LE\]>', timeout=3)
Ici mac est l'adresse du coquelicot de la théière.
Nous rompons la connexion:
child.sendline("exit")
- Abonnez-vous aux notifications
Après avoir établi la connexion, tout d'abord, nous devons nous abonner pour recevoir des notifications de la bouilloire. Sans cela, la théière percevra les commandes, mais elle ne pourra rien nous répondre sauf le texte «Avec succès».
child.sendline("char-write-cmd 0x000c 0100") child.expect(r'\[LE\]>')
- Se connecter
child.sendline("char-write-req 0x000e 55" + iter + "ff" + key + "aa") child.expect("value: ") child.expect("\r\n") connectedStr = child.before[0:].decode("utf-8") answer = connectedStr.split()[3]
Ci-après, iter est une variable hexadécimale itérative entière de 0 à 64 (de 0 à 100 dans le système décimal). Après chaque commande (réussie et non réussie), cette variable doit être augmentée de 1; lorsqu'elle atteint 64, elle est à nouveau réinitialisée à 0; clé - clé d'autorisation hexadécimale de 8 octets (par exemple: ffffffffffffffff).
Exemple de réponse:
valeur: 55 00 ff 01 aa
Le quatrième octet (01) signifie que la bouilloire vous a autorisé, sinon la réponse sera 00.
- De la magie de la rue
Après autorisation, une demande «magique» est toujours envoyée, dont l'essence n'est pas claire pour moi. Il existe une théorie selon laquelle il est nécessaire de "maintenir" l'état connecté. Apparemment, si vous ne l'envoyez pas, la déconnexion se produit en une seconde et vous devez tout recommencer. Si vous l'envoyez, le délai augmente considérablement, atteignant jusqu'à une douzaine de secondes. Confirmer de manière fiable cela, je ne pouvais pas.
child.sendline("char-write-req 0x000e 55" + iter + "01aa") child.expect("value: ") child.expect("\r\n") child.expect(r'\[LE\]>')
Exemple de réponse:
valeur: 55 01 01 02 1d aa
Dans toutes mes expériences, la réponse a toujours été la suivante.
UPD: dans les commentaires, ils ont suggéré que ce n'était pas du tout magique, mais simplement demander la version du logiciel; en conséquence, cette version est contenue dans la réponse. Ainsi, cette demande peut généralement être supprimée car inutile.
- Sync
Une commande qui synchronise l'heure dans une théière avec une horloge de serveur. Elle a encore un effet. Dans la bouilloire, il est possible d'afficher la température actuelle en mode veille en faisant clignoter une LED d'une certaine couleur. Cette fonction ne fonctionne qu'après synchronisation. Pour une description de la fonction elle-même, voir le paragraphe 11.
child.sendline("char-write-req 0x000e 55" + iter + "6e" + timeNow + tmz + "0000aa") child.expect("value: ") child.expect("\r\n") child.expect(r'\[LE\]>')
Ici, tmz est le fuseau horaire au format hexadécimal inversé (par exemple, traduisez le fuseau horaire +3 en secondes, puis au format hexadécimal et obtenez hexadécimal (3 * 60 * 60) = 2a30, divisé en paires et produit 302a en ordre inverse). Je ne sais pas quoi faire avec les fuseaux horaires négatifs, je ne l'ai pas testé, mais on soupçonne que le prochain octet tmz en est responsable. Ici, timeNow est l'heure unixtime actuelle au format hexadécimal inversé. L'algorithme est le même: nous obtenons l'heure actuelle en secondes, la traduisons en HEX, la divisons en paires et la sortons en ligne dans l'ordre inverse.
Exemple de réponse:
valeur: 55 02 6e 00 aa
Dans toutes mes expériences, la réponse a toujours été la suivante.
- Statistiques
La bouilloire a un mètre d'électricité consommée, la durée totale de fonctionnement et le nombre de démarrages. Si quelqu'un n'a pas besoin de ces données, vous pouvez ignorer cet élément en toute sécurité.
child.sendline("char-write-req 0x000e 55" + iter + "4700aa") child.expect("value: ") child.expect("\r\n") statusStr = child.before[0:].decode("utf-8") Watts = hexToDec(str(statusStr.split()[11] + statusStr.split()[10] + statusStr.split()[9])) alltime = round(self._Watts/2200, 1) child.expect(r'\[LE\]>') child.sendline("char-write-req 0x000e 55" + iter + "5000aa") child.expect("value: ") child.expect("\r\n") statusStr = child.before[0:].decode("utf-8") times = hexToDec(str(statusStr.split()[7] + statusStr.split()[6])) child.expect(r'\[LE\]>')
Watts - renvoie l'énergie consommée en Wh * h, en tout temps - les heures de fonctionnement de la bouilloire, fois - le nombre de démarrages de la bouilloire. hexToDec - une fonction de conversion au format décimal.
- Lire le mode de fonctionnement actuel
child.sendline("char-write-req 0x000e 55" + iter + "06aa") child.expect("value: ") child.expect("\r\n") statusStr = child.before[0:].decode("utf-8") answer = statusStr.split() status = str(answer[11]) temp = hexToDec(str(answer[8])) mode = str(answer[3])
Exemple de réponse:
valeur: 55 04 06 00 00 00 00 01 2a 1e 00 00 00 00 00 00 80 00 00 aa
Le quatrième octet est le mode de fonctionnement (mode): 00 - ébullition, 01 - chauffage à température, 03 - veilleuse. Le sixième octet est la température hexadécimale à laquelle il est nécessaire de chauffer en mode chauffage, en mode ébullition, il est 00. Le neuvième octet est la température hexadécimale actuelle de l'eau (2a = 42 degrés Celsius). Le douzième octet est l'état de la théière: 00 - éteint, 02 - allumé. Le dix-septième octet est la durée de la bouilloire après avoir atteint la température souhaitée, par défaut, elle est de 80 en hexadécimal (apparemment, ce sont des unités relatives, certainement pas des secondes).
- Enregistrer le mode de fonctionnement actuel
child.sendline("char-write-req 0x000e 55" + iter + "05" + mode + "00" + temp + "00000000000000000000" + howMuchBoil + "0000aa") child.expect("value: ") child.expect("\r\n") statusStr = child.before[0:].decode("utf-8") answer = statusStr.split()[3] child.expect(r'\[LE\]>')
Mode paramètres: 00 - ébullition, 01 - chauffage à température, 03 - veilleuse. Le paramètre temp est la température hexadécimale à laquelle il est nécessaire de chauffer en mode «chauffage», en mode ébullition, il est 00. Le paramètre howMuchBoil est la durée de la bouilloire après avoir atteint la température souhaitée, la valeur par défaut est 80 en hexadécimal (apparemment, ce sont des unités relatives , certainement pas quelques secondes).
Exemple de réponse:
valeur: 55 05 05 01 aa
Le quatrième octet de la réponse indique la réussite des réglages: 01 - réussi, 00 - échoué.
- Exécuter le mode de fonctionnement actuel
child.sendline("char-write-req 0x000e 55" + iter + "03aa") child.expect("value: ") child.expect("\r\n") statusStr = self.child.before[0:].decode("utf-8") answer = statusStr.split()[3] child.expect(r'\[LE\]>')
Exemple de réponse:
valeur: 55 06 03 01 aa
Le quatrième octet de la réponse indique le succès de l'inclusion: 01 - réussi, 00 - non réussi.
- Arrêter le mode de fonctionnement actuel
child.sendline("char-write-req 0x000e 55" + iter + "04aa") child.expect("value: ") child.expect("\r\n") statusStr = self.child.before[0:].decode("utf-8") answer = statusStr.split()[3] child.expect(r'\[LE\]>')
Exemple de réponse:
valeur: 55 07 04 01 aa
Le quatrième octet de la réponse indique la réussite de l'arrêt: 01 - avec succès, 00 - sans succès.
- Afficher la température actuelle en couleur au repos
child.sendline("char-write-req 0x000e 55" + iter + "37c8c8" + onoff + "aa")
Le paramètre onoff est soit 01 pour activer la fonction, soit 00 pour désactiver la fonction.
Exemple de réponse:
valeur: 55 08 37 00 aa
Dans toutes mes expériences, la réponse a toujours été la suivante.
- Enregistrer une palette de couleurs de différents modes de fonctionnement
Une palette de correspondance entre la couleur de la LED et la température est réglée dans le mode d'affichage de la température actuelle et des modes de chauffage et d'ébullition, ainsi que la palette de couleurs dans le mode veilleuse.
child.sendline("char-write-req 0x000e 55" + iter + "32" + boilOrLight + scale_from + rand + rgb1 + scale_mid + rand + rgb_mid + scale_to + rand + rgb2 + "aa") child.expect("value: ") child.expect("\r\n") child.expect(r'\[LE\]>')
Le paramètre boilOrLight est 00 si nous réglons le mode d'affichage de la température actuelle ou 01 si nous réglons le mode nuit. Le paramètre scale_from indique le début de la plage de changement de couleur et est égal à 00 en mode veilleuse et 28 en mode d'affichage de la température actuelle (28 est 40 au format décimal et c'est à partir de cette température qu'un changement de couleur en douceur va commencer). Le paramètre scale_mid est le milieu de la plage et est 32 en mode veilleuse et 46 en mode d'affichage de la température actuelle. Le paramètre scale_to indique la fin de la gamme de couleurs et vaut 64 dans les deux modes. Le paramètre rgb1 est la couleur hexadécimale du début de la palette. Le paramètre rgb_mid est la couleur hexadécimale du milieu de la palette (je le calcule comme le milieu entre les extrémités gauche et droite, mais théoriquement, vous pouvez spécifier n'importe quelle couleur, cela n'affectera que la beauté et la douceur du changement de couleur). Le paramètre rgb2 est la couleur hexadécimale de la fin de la palette. Le paramètre rand est un certain paramètre, dont je n'ai pas compris la valeur exactement, peut-être en quelque sorte lié à la luminosité de la couleur (exemples de valeurs: e5, cc).
Exemple de réponse:
valeur: 55 09 32 00 aa
Dans toutes mes expériences, la réponse a toujours été la suivante.
- Lisez la palette de couleurs des différents modes de fonctionnement
child.sendline("char-write-req 0x000e 55" + iter + "33" + boilOrLight + "aa") child.expect("value: ") child.expect("\r\n") statusStr = self.child.before[0:].decode("utf-8") child.expect(r'\[LE\]>')
Le paramètre boilOrLight peut être 00 - si nous réglons le mode d'affichage de la température actuelle ou 01 - si nous réglons le mode nuit.
Exemple de réponse:
valeur: 55 10 33 01 00 7f 00 00 ff 32 7f 00 ff 00 64 7f ff 00 00 aa
Ici, les sixième, onzième et seizième octets (7f) sont le paramètre rand de l'élément 12. Le cinquième octet est scale_from, le dixième octet est scale_mid, le quinzième octet est scale_to. Les septième + huitième + neuvième octets sont rgb_from. Les douzième + treizième + quatorzième octets sont rgb_mid. Dix-septième + dix-huitième + dix-neuvième octets - rgb_to.
Conclusion
Si gatttool ne veut pas se connecter à la théière (c'est possible la première fois que vous vous connectez à des périphériques inconnus), essayez de rechercher la théière en utilisant os avant de connecter le module:
sudo hciconfig device reset sudo timeout 1 hcitool lescan
appareil - identifiant de votre appareil Bluetooth (par exemple, hci0). Assurez-vous que l'adresse du pavot de votre bouilloire est dans la liste des appareils trouvés. Après cela:
sudo hcitool lewladd mac sudo hcitool lerladd mac
mac - l'adresse du pavot de votre théière
UPD6 : amélioration significative du module bouilloire:
1. Transféré le module de la plateforme en mode d'intégration
2. Après l'ajout, vous aurez automatiquement 3 éléments: un chauffe-eau (température actuelle, température cible, ébullition et chauffage), un capteur (temps de synchronisation, énergie dépensée, heures de fonctionnement, nombre de démarrages) et la lumière (peut être utilisée comme lampe de nuit et choisir n'importe quelle couleur rétro-éclairage)
3. Le module est maintenant disponible sur
GitHub .
4. Le module prend en charge l'installation via
HACS5. Exemple de configuration:
r4s_kettler: device: 'hci0' mac: 'FF:FF:FF:FF:FF:FF' password: 'ffffffffffffffff'
Captures d'écran de la nouvelle version UPD7 : informations non pertinentes
supprimées