Vous venez d’installer une pompe à chaleur comme moyen de chauffage principal et votre facture EDF a vu son montant grimper en flèche pendant les heures de pointe. Si votre pompe à chaleur est connectée à Internet, vous avez la possibilité de contrôler la consigne de température via un site web ou une application sur smartphone. Mais comment automatiser ce contrôle lors des changements de période tarifaire de votre fournisseur d’énergie ? Une bonne occasion de se mettre au langage Rust !
Nous avons fait l’acquisition d’une pompe à chaleur Mitsubishi équipée d’un module Wi-Fi connecté au réseau local et donc à Internet via notre box ; ce module permet de la contrôler via un serveur (MELCloud) dont l’accès est mis à disposition par le fabricant. Notre contrat EDF est un contrat EJP dont la tarification est particulièrement intéressante pendant les heures dites « normales », mais qui impose 22 jours par an une tarification forte pour des heures dites « pointe mobile » entre 7 h du matin et 1 h du matin le jour suivant.
L’objet de cet article est de montrer comment automatiser une baisse de la consigne de température de la pompe à chaleur au début des heures de pointe mobile (à 7 h du matin les jours EJP) et de rétablir la consigne d’origine à la fin de cette période (à 1 h du matin le jour suivant). Il va de soi que la technique envisagée est adaptable à tout type de contrat d’énergie alternant des heures pleines/creuses.
1. Le problème à résoudre et la solution retenue
Plusieurs solutions existent pour la détection des changements de période tarifaire :
- Utiliser le contact sec heures creuses/pleines du compteur électrique : cette solution est la plus simple, mais nécessite une liaison filaire au compteur et suppose que ce contact n’est pas déjà utilisé pour délester un chauffe-eau électrique, par exemple.
- Utiliser un décodeur de trame Pulsadis : on peut trouver dans le commerce des décodeurs de trames Pulsadis (voir note) qui fournissent un contact sec analogue au précédent, mais qui présentent l’avantage de pouvoir être branché en un point quelconque de l’installation électrique, car ces trames ne sont pas filtrées par le compteur et sont donc disponibles sur toutes les prises. Pour les amateurs de montages électroniques et adeptes du fer à souder, sachez qu’il est tout à fait possible d’entreprendre la réalisation de ce genre de décodeur (voir référence [7]).
Pulsadis
Le système de télécommande Pulsadis permet (bien antérieurement aux compteurs Linky) de déclencher les changements de période tarifaire des compteurs (EJP, Tempo, heures pleines/heures creuses...) et de commander des services pour la clientèle (charges associées aux tarifs) ainsi que des usages collectifs comme l’éclairage public, par exemple. Ce système, toujours en service, délivre ses commandes en superposant un signal à 175 Hz au secteur, modulé par des impulsions binaires : il s’agit de la TCFM (Télécommande Centralisée à Fréquence Musicale).
La figure 1 montre la structure d’une trame (40 bits utiles après le bit de départ).
Les informations sur la période EJP sont fournies par les bits 5 et 15 :
- bit 5 présent seul : signale une alerte la veille d’un jour EJP ;
- bits 5 et 15 présents : signalent le début des heures de pointe mobile ;
- bit 15 présent seul : signale le retour aux heures normales.
On trouvera en figure 2 le détail des commandes TCFM associées aux 40 bits significatifs de la trame d’après la spécification technique d’EDF.
Comme on peut le constater sur la figure 1, la durée d’une trame étant de 102,25 s, la période de scrutation de l’état du contact n’est donc pas critique.
- Analyser les trames de téléinformation client (TIC) disponible sur le bus de téléinformation du compteur : cette solution nécessite une liaison filaire sur le bus TIC via un filtre permettant de supprimer la porteuse et connecté sur la liaison série d’un Raspberry, par exemple ; on pourra trouver une description de cette solution en référence [1].
Disposant d’un décodeur Pulsadis de récupération (fabriqué par la société Krigelec qui malheureusement n’existe plus), nous avons choisi la deuxième solution. Le contact sec du décodeur est relié aux broches GPIO d’un Raspberry Pi 3 Model B disposant d’une interface Wi-Fi intégrée. La liaison avec le contact utilise la broche GPIO 4 (en raison de son voisinage avec une broche de masse pour faciliter l’usage d’un connecteur standard) et la broche de masse voisine ; la broche GPIO 4 est utilisée en entrée avec une résistance de tirage (pull-up) interne (le contact utilisé est ouvert en période « heure normale »). La photo montre la maquette réalisée en fonctionnement, la connexion à Internet via l’interface Wi-Fi du Raspberry et la box du réseau local nous permet de gérer les requêtes au serveur MELCloud.
Nous avons choisi de développer l’application en langage Rust ; ce choix résulte de la volonté de mettre sérieusement le pied à l’étrier pour l’utilisation de ce nouveau langage. Il convient donc d’installer la chaîne de développement Rust sur notre Raspberry ; nous suivons en cela les indications fournies sur le site du langage (voir référence [2]) et une fois connectés au Raspberry via SSH, nous tapons la commande :
Ceci lance l’installation pour laquelle nous choisissons les options par défaut. Nous pouvons ensuite vérifier l’opérationnalité du compilateur avec la commande rustc --version et celle du gestionnaire de paquets avec cargo --help.
Nous pouvons alors passer à la conception en créant un répertoire pour notre projet.
2. Détection de la période tarifaire avec le paquet RPPAL
Nous allons tout d’abord mettre en œuvre la liaison avec notre décodeur Pulsadis au moyen d’un programme de test test-ejp ; pour cela, nous tapons les commandes suivantes à partir du répertoire de notre projet :
Nous allons utiliser le paquet RPPAL – Raspberry Pi Peripheral Access Library dont on trouvera la documentation et des exemples d’utilisation en consultant la référence [3]. Les commandes précédentes ont créé un projet de type « Bonjour » ; il suffit d’éditer le fichier Cargo.toml du répertoire courant et d’y ajouter la dépendance adéquate :
Nous modifierons également le programme créé par défaut en éditant le fichier src/main.rs de notre projet :
Les lignes 1 à 6 permettent d’alléger les références aux éléments des modules utilisés dans le code qui suit.
Notre programme de test, après configuration de la broche GPIO 4, lit le niveau haut ou bas de cette broche 10 fois avec un intervalle de 5 secondes donnant le temps de manipuler le contacteur EJP (par forçage puisqu’il n’est pas question ici d’attendre la contribution d’EDF).
C’est la ligne 11 qui réalise la configuration de la broche GPIO 4 avec une résistance de tirage interne. La méthode Gpio::new() renvoie une structure Gpio qui peut être ensuite affectée à la broche 4, puis connectée à la résistance de tirage par l’enchaînement des appels des méthodes correspondantes. On notera la présence du caractère « ? » à la suite des appels pouvant échouer pour permettre cet enchaînement ; l’utilisation de cette possibilité impose la déclaration de main() comme retournant un résultat qui peut être une erreur (ligne 8) et la nécessité de fournir un résultat correct lorsque tout se passe bien (ligne 20). On peut observer ce qui se passe à l’exécution en remplaçant le 4 par 255 par exemple :
La lecture de la broche s’effectue à la ligne 13 par appel de la méthode pin.read() qui retourne une énumération testée avec la structure de contrôle match afin d’afficher le message adéquat selon le niveau de la broche.
On peut alors compiler le projet à l’aide de la commande cargo build et l’exécuter avec cargo run, ce qui doit fournir la sortie suivante sur le terminal si la compilation n’a pas détecté d’erreur, et en forçant le contact EJP (ou en court-circuitant les broches GPIO 4 et masse avec un strap) après le démarrage :
La première compilation est relativement longue en raison de la dépendance avec le paquet rppal qui doit être importé et compilé une première fois ; il en sera de même au chapitre suivant lors de l’introduction des nouveaux paquets concernant les requêtes HTTP et JSON : patience !
3. Gestion des requêtes MELCloud avec les paquets REQWEST et SERDE_JSON
Il nous reste maintenant à utiliser les services MELCloud pour mettre à jour la consigne de température. Il nous faut pour cela analyser les requêtes HTTP échangées avec le serveur en ouvrant la page https://app.melcloud.com dans un navigateur internet en ayant activé les options de développement. Sous réserve de disposer d’un compte valide sur le serveur pour s’y connecter, on obtient le résultat montré sur la figure 3.
3.1 La connexion au service MELCloud
L’analyse de l’historique des requêtes nous fournit la méthode d’identification utilisée : la procédure de login fournit un jeton de session qui doit être transmis dans l’entête de chacune des futures requêtes au serveur. Nous choisissons donc de mémoriser ce jeton dans une structure ClientMelcloud dont les méthodes implémenteront les différentes requêtes utiles à notre application ; cette structure sera codée dans le module src/melcloud.rs.
Commençons par créer un programme de test « test-melcloud » dans le répertoire racine de notre projet à l’aide des commandes cargo new test-melcloud, puis cd test-melcloud.
Comme notre application ne nécessite aucune interactivité utilisateur, nous utiliserons la version synchrone du client HTTP fourni par le paquet REQWEST [4] ; l’analyse des requêtes montre par ailleurs des échanges de données au format JSON, d’où l’utilisation du paquet SERDE_JSON [5]. Ceci nous conduit à ajouter les dépendances suivantes au fichier Cargo.toml :
Passons à l’écriture de notre module src/melcloud.rs ; nous y importons les éléments des paquets utilisés (on notera la définition d’un alias pour serde_json::from_str afin d’éviter toute ambiguïté) et définissons notre structure suivie de son implémentation.
L’URL de base de nos services y est mémorisée comme une constante statique.
La fonction new() construit une instance de la structure, initialisée avec le jeton de session en cas de succès au moyen d’un appel à la méthode privée connecter(). Il n’y a aucun moyen de continuer en cas d’échec ; nous choisissons donc de traiter les erreurs par des appels à expect() ou panic().
Concernant la connexion, nous préparons une chaîne de caractères au format JSON qui sera le corps de la requête POST ; nous avons choisi une chaîne brute afin de rendre l’insertion des guillemets plus lisible. Outre l’identifiant et le mot de passe, la requête de login requiert un numéro de version pour l’application : nous avons choisi le même que l’application web. Si les URL sont correctes et la connexion internet opérationnelle, on obtient logiquement une réponse du serveur avec un statut « 200 OK » ; on peut alors extraire les données utiles de la réponse au moyen de la fonction json_from_str() qui ne devrait pas échouer.
La principale raison d’échec est une identification incorrecte ; dans ce cas, la réponse du serveur contient un champ « ErrorId » non nul, sinon il ne reste plus qu’à extraire le jeton de session.
On notera que données["LoginData"]["ContextKey"] est une serde_json::Value dont le contenu est de la forme « \"5B … 9F\" » guillemets inclus, d’où l’appel à as_str().unwrap() pour les éliminer.
Il nous reste à tester la connexion en créant une instance de ClientMelcloud dans notre fichier src/main.rs :
La commande cargo run fournit alors le résultat en cas d’identification réussie :
En cas d’échec identification, on obtient :
Si le format des données du corps de la méthode POST est incorrect, on affiche :
Et sans connexion internet :
3.2 La gestion des équipements
Afin de changer la température de consigne d’un équipement de l’installation, il convient d’interroger le serveur pour obtenir ses paramètres courants (requête Get) pour les modifier ensuite (requête SetAtw) ; la requête Get a besoin de paramètres identifiant un équipement dans une installation. Un utilisateur peut posséder plusieurs installations (une installation est associée à un lieu) comportant chacune plusieurs équipements. Il faut donc obtenir du serveur la liste des identifiants de tous les équipements de chaque installation ; ceci sera la responsabilité de la méthode liste_équipements() dont nous mémoriserons le résultat dans notre structure ClientMelcloud.
Les identifiants sont des valeurs numériques que nous représenterons par le type u64 en raison du traitement des nombres par le paquet SERDE_JSON. Nous mémoriserons ces valeurs comme un tableau d’installations définies chacune par son identifiant et un tableau des identifiants des équipements.
Nous allons donc modifier notre structure comme ci-dessous :
Nous modifierons ensuite la méthode new() afin qu’elle initialise ce nouveau champ par un appel à une nouvelle méthode liste_équipements() ; nous y avons inséré l’affichage des équipements à des fins de débogage :
Il nous reste à coder la méthode liste_équipements() ; on notera l’entête concernant le jeton de session (celui-ci doit être passé en argument, car la structure n’est pas encore initialisée au moment de l’appel) :
En cas d’échec, cette requête fournit une réponse différente ; la présence ou non de la chaîne « Success : False » permet de déterminer le type de la réponse. En cas de succès, on fait appel aux méthodes de la structure Value du paquet SERDE_JSON pour construire le résultat en se référant à l’étude des données affichées dans le volet développement du navigateur évoqué au début du chapitre 3.
Voici la sortie du programme de test pour une seule installation comportant un unique équipement :
3.3 Le contrôle de la température
Notre propos étant de modifier la température de consigne de tous les équipements, nous allons ajouter la méthode publique ajuster_température() à notre structure ; celle-ci fera appel à deux méthodes privées : lire_données() exécutant la requête Get et écrire_données() exécutant la requête SetAtw. Voici le squelette de la méthode ajuster_température() (on notera cette fois l’argument &self : il s’agit d’une méthode d’instance qui doit avoir accès aux membres de la structure ClientMelcloud, de même que lire_données() et écrire_données()) :
Quelle représentation choisir pour les données de l’équipement (valeur de retour de la méthode lire_données() ? La requête Get fournit en réponse des données au format JSON sous forme d’une chaîne de caractères ; on pourrait choisir un type serde_json::Value qui permettrait un accès direct aux données utiles, valeur qui pourraient être passée à écrire_données() après modification pour sérialisation dans le corps de la requête SetAtw. En fait, cela pose des problèmes d’emprunt pour la structure de type Value qui serait la possession de la méthode lire_données(). Nous avons donc choisi le type String pour le passage des données entre méthodes, la manipulation du format JSON s’effectuant au sein la méthode ajuster_température() dont voici finalement le codage complet :
Chacun des équipements installés peut gérer deux zones d’habitation dont nous mettons à jour la consigne de température selon le paramètre delta. Nous devons en outre positionner les drapeaux adéquats dans le champ "EffectiveFlags" avant de renvoyer les données au serveur.
Reste à coder les requêtes de lecture/écriture, ce qui ne pose aucune difficulté après l’expérience acquise précédemment :
On pourra tester le module complet à l’aide du programme de test suivant (fichier main.rs) :
Et constater la mise à jour des consignes directement sur l’unité de gestion de la pompe à chaleur quelques minutes plus tard.
4. La touche finale
Maintenant que notre module melcloud est opérationnel, il nous reste à assembler les morceaux dans l’application finale. Tout d’abord, nous envisageons de mémoriser les paramètres de l’application dans un fichier du répertoire de travail plutôt que dans le code comme lors des tests ; nous choisirons le format JSON pour cela. Voici le contenu de ce fichier (config.json) :
Nous définirons par ailleurs un type ÉtatEJP afin de fournir un niveau d’abstraction plus élevé que le niveau haut/bas de la broche GPIO. On remarquera l’attribut #[derive(PartialEq)] de ce type énuméré rendant possible la comparaison entre deux instances de ce type (par implémentation automatique du trait).
Après chargement des paramètres, nous effectuons une scrutation du contact EJP toutes les 5 minutes (la durée d’une trame Pulsadis est d’environ 120 secondes) afin de détecter les changements de période tarifaire et réaliser le changement adéquat de la consigne de température. Voici donc la version finale du fichier main.rs :
Nous avons choisi d’afficher les changements d’état EJP sur la sortie standard ; si nous exécutons cette application en tant que service via systemctl, la trace sera visible dans /var/log/syslog.
Conclusion
Cet article nous a permis de nous familiariser avec quelques aspects du langage Rust, en particulier pour la gestion des GPIO sur le Raspberry Pi et la réalisation d’un client HTTP avec échange de données au format JSON. Et nous espérons évidemment faire quelques économies lors de la prochaine saison hivernale.
Références
[1] M. Rambouillet, « Modélisation d’un système de téléinformation EDF », GNU/Linux Magazine n°196 -
https://connect.ed-diamond.com/GNU-Linux-Magazine/glmf-196/modelisation-d-un-systeme-de-teleinformation-edf
et GNU/Linux Magazine n°197 - https://connect.ed-diamond.com/GNU-Linux-Magazine/GLMF-197/Conception-d-un-systeme-de-tele-information-EDF
[2] Installation de Rust : https://www.rust-lang.org/fr/tools/install
[3] Documentation du paquet RPPAL : https://github.com/golemparts/rppal
[4] Documentation du paquet REQWEST : https://github.com/seanmonstar/reqwest
[5] Documentation du paquet SERDE_JSON : https://github.com/serde-rs/json
[6] Le langage Rust : https://www.rust-lang.org/fr
[7] Réalisation d’un décodeur d’impulsions EJP : http://matthieu.benoit.free.fr/pulsadis.htm