Contrôlez votre pompe à chaleur selon la période tarifaire EDF

Magazine
Marque
Hackable
Numéro
44
Mois de parution
septembre 2022
Spécialité(s)


Résumé

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 !


Body

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

pulsadisGROS-s

Fig. 1 : La trame Pulsadis.

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.

commandes-tcfm-s

Fig. 2 : Les commandes TCFM.

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.

maquette-s

La maquette en fonctionnement.

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 :

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

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 :

cargo new test-ejp
cd test-ejp

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 :

[dependencies]
rppal = { version = "^0.13.1" }

Nous modifierons également le programme créé par défaut en éditant le fichier src/main.rs de notre projet :

01: use std::error::Error;
02: use std::thread;
03: use std::time::Duration;
04:
05: use rppal::gpio::Gpio;
06: use rppal::gpio::Level;
07:
08: fn main() -> Result<(), Box<dyn Error>> {
09:     println!("TEST EJP");
10:
11:     let pin = Gpio::new()?.get(4)?.into_input_pullup();
12:     for _ in 0..10 {
13:         match pin.read() {
14:             Level::Low => println!("HEURES DE POINTE MOBILE"),
15:             Level::High => println!("HEURES NORMALES")
16:             }
17:         thread::sleep(Duration::from_secs(5));
18:     }
19:
20:     Ok(())
21: }

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 :

TEST EJP
Error: PinNotAvailable(255)

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 :

TEST EJP
HEURES NORMALES
HEURES DE POINTE MOBILE
...

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.

melcloud-s

Fig. 3 : Analyse des requêtes HTTP.

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 :

[dependencies]
reqwest = { version = "0.11.10", features = ["blocking"] }
serde_json = "^1.0"

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.

use reqwest::blocking::Client;
use reqwest::header::*;
use serde_json::{Value, from_str as json_from_str, to_value as json_to_value};
 
pub struct ClientMelCloud {
    jeton: String,
}
 
impl ClientMelCloud {
    const URL_SERVEUR: &'static str =
            "https://app.melcloud.com/Mitsubishi.Wifi.Client/";
 
    pub fn new(utilisateur: String, secret: String) -> ClientMelCloud {
        let jeton = ClientMelCloud::connecter(utilisateur, secret);
        println!("JETON:{}", jeton); // Pour le test
 
        ClientMelCloud {
            jeton: jeton,
        }
    }
 
    fn connecter(utilisateur: String, secret: String) -> String {
        // Préparer les données nécessaires à la connexion
        // Un numéro de version est obligatoire
        let infos_login = format!(r#"{{
            "Email":"{}",
            "Password":"{}",
            "AppVersion":"1.25.0.1"
        }}"#, utilisateur, secret);
 
        // Contacter le service de connexion
        let client = Client::new();
        let mut url_login = String::from(ClientMelCloud::URL_SERVEUR);
        url_login.push_str("Login/ClientLogin");
        let réponse = client.post(url_login)
            .header(CONTENT_TYPE, "application/json; charset=utf-8")
            .body(infos_login)
            .send();
 
        // Traiter la réponse
        if let Ok(réponse) = réponse {
            if !réponse.status().is_success() {
                panic!("LOGIN : ERREUR ACCÈS SERVEUR";
            }
            if let Ok(réponse) = réponse.text() {
                let données : Value = json_from_str(&réponse)
                   .expect("Réponse login : format JSON incorrect");
                if données["ErrorId"].to_string() != "null" {
                    panic!("LOGIN: ERREUR D'IDENTIFICATION");
                }
                // Fournir le jeton de session en retour
                données["LoginData"]["ContextKey"].as_str().unwrap().to_string()
            } else {
                panic!("LOGIN: ERREUR RÉPONSE MALFORMÉE");
            }
        } else {
            panic!("LOGIN: ERREUR POST");
        }
    }
}

Il nous reste à tester la connexion en créant une instance de ClientMelcloud dans notre fichier src/main.rs :

mod melcloud;
 
 
fn main() {
    println!("PROTOTYPE D'ACCÈS À MELCLOUD");
 
    let _client = melcloud::ClientMelCloud::new(
        "identifiant MELCloud".to_string(),
        "mot de passe".to_string()
    );
 
    println!("CLIENT MELCLOUD PRÊT");
}

La commande cargo run fournit alors le résultat en cas d’identification réussie :

PROTOTYPE D'ACCÈS À MELCLOUD
JETON:88D1C1735A664AE5B2453CECAE5569
CLIENT MELCLOUD PRÊT

En cas d’échec identification, on obtient :

thread 'main' panicked at 'LOGIN: ERREUR D'IDENTIFICATION', src/melcloud.rs:81:63

Si le format des données du corps de la méthode POST est incorrect, on affiche :

thread 'main' panicked at 'LOGIN : ERREUR ACCÈS SERVEUR', src/melcloud.rs:77:17

Et sans connexion internet :

thread 'main' panicked at 'LOGIN: ERREUR POST', src/melcloud.rs:85:18

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 :

pub struct ClientMelCloud {
    jeton: String,
    équipements_utilisateur: Vec<(u64,Vec<u64>)>
}

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 :

pub fn new(utilisateur: String, secret: String) -> ClientMelCloud {
    let jeton = ClientMelCloud::connecter(utilisateur, secret);
    let liste = ClientMelCloud::liste_équipements(&jeton);
 
    println!("LISTE DES ÉQUIPEMENTS :"); // Pour le débogage
    for (id_installation, équipements) in &liste {
        println!("\tINSTALLATION : {}", id_installation);
        for id_équipement in équipements {
            println!("\t\tÉQUIPEMENT : {}", id_équipement);
        }
    }
 
    ClientMelCloud {
        jeton: jeton,
        équipements_utilisateur: liste
    }
}
 

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

fn liste_équipements(jeton: &String) -> Vec<(u64,Vec<u64>)> {
    // Contacter le service MelCloud « liste des équipements »
    let client = Client::new();
    let mut url_lister = String::from(ClientMelCloud::URL_SERVEUR);
    url_lister.push_str("User/ListDevices");
    let réponse = client.get(url_lister)
        .header("X-MitsContextKey", jeton)
        .send();
 
    // Extraire les données JSON de la réponse
    let réponse = réponse.unwrap().text().unwrap();
    if réponse.contains("Success: False") {
        panic!("ÉQUIPEMENTS : ERREUR ACCÈS SERVEUR");
    }
    let équipements : Value = json_from_str(&réponse)
        .expect("Réponse équipements : format JSON incorrect");
 
    // Construire la liste des équipements
    let mut liste_installations : Vec<(u64,Vec<u64>)> = Vec::new();
    for installation in équipements.as_array().unwrap() {
        let mut liste_équipements : Vec<u64> = Vec::new();
        for équipement in installation["Structure"]["Devices"].as_array().unwrap() {
            liste_équipements.push(équipement["DeviceID"].as_u64().unwrap());
        }
        liste_installations.push(
            (installation["ID"].as_u64().unwrap(),
             liste_équipements));
    }
 
    // Fournir le résultat
    liste_installations
}

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 :

PROTOTYPE D'ACCÈS À MELCLOUD
LISTE DES ÉQUIPEMENTS :
    INSTALLATION : 315012
        ÉQUIPEMENT : 23909425
CLIENT MELCLOUD PRÊT

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()) :

pub fn ajuster_température(&self, delta: f64) {
    for (installation, équipements_installation) in &self.équipements_utilisateur {
        for équipement in équipements_installation {
            let données_équipement = &self.lire_données(installation, équipement);
            /* Modifier les données */
            let _ = &self.écrire_données(installation, équipement, données_équipement);
        }
    }
}

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 :

pub fn ajuster_température(&self, delta: f64) {
    for (installation,équipements_installation) in &self.équipements_utilisateur {
        for équipement in équipements_installation {
            // Ajuster les consignes
            let données_équipement = &self.lire_données(installation, équipement);
            let mut données_équipement: Value = json_from_str(données_équipement)
                .expect("Réponse équipement : format JSON incorrect");
            let consigne1 = données_équipement["SetTemperatureZone1"]
                                    .as_f64().unwrap();
            let consigne2 = données_équipement["SetTemperatureZone2"]
                                    .as_f64().unwrap();
            données_équipement["SetTemperatureZone1"] =       
                    json_to_value(consigne1 + delta).unwrap();
            données_équipement["SetTemperatureZone2"] =
                    json_to_value(consigne2 + delta).unwrap();
 
            // Fixer les indicateurs de tâche et faire la mise à jour sur le serveur
            données_équipement["EffectiveFlags"] =
                    json_to_value(0x200000080u64).unwrap();
            let _ = &self.écrire_données(données_équipement.to_string());
        }
    }
}

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 :

fn lire_données(&self, installation: &u64, équipement: &u64) -> String {
    // Contacter le service données des équipements
    let client = Client::new();
    let mut url_lire = String::from(ClientMelCloud::URL_SERVEUR);
    url_lire.push_str(format!("Device/Get?id={}&buildingID={}",
                               équipement, installation).as_str());
    let réponse = client.get(url_lire)
        .header("X-MitsContextKey", &self.jeton)
        .send();
 
    // Extraire les données de la réponse et retourner le résultat en cas de succès
    let réponse = réponse.unwrap().text().unwrap();
    if réponse.contains("Success: False") {
        panic!("ÉQUIPEMENT {} / {} : ERREUR ACCÈS SERVEUR", installation, équipement);
    }
    réponse.to_string()
}
 
fn écrire_données(&self, valeurs: String) {
    // Contacter le service données des équipements
    let client = Client::new();
    let mut url_écrire = String::from(ClientMelCloud::URL_SERVEUR);
    url_écrire.push_str("Device/SetAtw");
    let réponse = client.post(url_écrire)
        .header("X-MitsContextKey", &self.jeton)
        .header(CONTENT_TYPE, "application/json; charset=utf-8")
        .body(valeurs)
        .send();
    let réponse = réponse.unwrap().text().unwrap();
    if réponse.contains("Success: False") {
        panic!("MAJ ÉQUIPEMENT : ERREUR ACCÈS SERVEUR");
    }
}

On pourra tester le module complet à l’aide du programme de test suivant (fichier main.rs) :

mod melcloud;
 
fn main() {
    println!("PROTOTYPE D'ACCÈS À MELCLOUD");
 
    let client = melcloud::ClientMelCloud::new(
        "identifiant MELCloud".to_string(),
        "mot de passe".to_string()
    );
    client.ajuster_température(-1.0);
 
    println!("CONSIGNES MISES À JOUR");
}

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

{
"Utilisateur":"identifiant MELCloud",
"Secret":"mot de passe",
"Variation":2.0,
"GPIO":4
}

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 :

mod melcloud;
 
use std::fs;
use std::thread;
use std::time::Duration;
 
use rppal::gpio::Gpio;
use rppal::gpio::Level;
 
#[derive(PartialEq)]
enum ÉtatEJP {
    HeureNormale,
    HeurePointe
}
 
impl ÉtatEJP {
    fn from_level(l: Level) -> ÉtatEJP {
        match l {
            Level::Low => ÉtatEJP::HeurePointe,
            Level::High => ÉtatEJP::HeureNormale
        }
    }
 
    fn to_string(&self) -> String {
        match &self {
            ÉtatEJP::HeureNormale => "Heure normale".to_string(),
            ÉtatEJP::HeurePointe => "Heure de pointe".to_string()
        }
    }
}
 
fn main() {
    // Charger les paramètres
    let configuration = fs::read_to_string("config.json")
        .expect("Fichier \"config.json\" inexistant");
    let configuration: serde_json::Value = serde_json::from_str(&configuration)
        .expect("Fichier : \"config.json\" format JSON incorrect");
 
    // Construire le client MelCloud
    let client = melcloud::ClientMelCloud::new(
        configuration["Utilisateur"].as_str().unwrap().to_string(),
        configuration["Secret"].as_str().unwrap().to_string()
    );
    println!("Client MelCloud prêt");
 
    // Configurer la broche GPIO du contact EJP
    let gpio = configuration["GPIO"].as_u64().unwrap() as u8;
    let pin = Gpio::new().expect("ERREUR ACCÈS GPIO").get(gpio)
        .expect("GPIO : ERREUR BROCHE").into_input_pullup();
    println!("Contact EJP sur broche {}", gpio);
 
    // Définir la variation de la consigne
    let variation_consigne = configuration["Variation"].as_f64().unwrap();
    println!("Variation consigne : {} °C", variation_consigne);
 
    // Définir l'état initial au lancement de l'application
    let mut état_ejp = ÉtatEJP::from_level(pin.read());
    println!("EJP : {}", état_ejp.to_string());
 
    loop {
        thread::sleep(Duration::from_secs(5 * 60)); // 5 minutes
        let nouvel_état_ejp = ÉtatEJP::from_level(pin.read());
 
        if nouvel_état_ejp != état_ejp {
            état_ejp = nouvel_état_ejp;
            println!("EJP -> {}", état_ejp.to_string());
            client.ajuster_température(match état_ejp {
                ÉtatEJP::HeureNormale => variation_consigne,
                ÉtatEJP::HeurePointe => -variation_consigne
            })
        }
    }
}

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 Magazine196 -
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



Article rédigé par

Par le(s) même(s) auteur(s)

Les tribulations d'un programmeur Linux dans la Sierra Apple

Magazine
Marque
GNU/Linux Magazine
Numéro
216
Mois de parution
juin 2018
Spécialité(s)
Résumé

Après développement d'applications domotiques basées sur des échanges de datagrammes pour divers hôtes (Raspberry Pi, ESP-12, smartphone Android) et divers langages (C++, Python, Java), on finit par être confronté au portage de ces applications dans l'univers Apple ; il est alors naturel de se tourner vers le langage Swift et sa librairie « Foundation » (le langage Swift créé par Apple et rendu public en 2014 est passé en open source en décembre 2015).L'utilisation du protocole UDP conduit à mettre en œuvre les « CFSocket » de la libraire « Foundation » avec leur fonction de rappel (callback) associée pour la réception des datagrammes. On est alors amené à manipuler des pointeurs (UnsafePointer et autres variantes du langage Swift) vers divers objets ce qui n'est pas évident a priori. Cet article est destiné à faire partager ce retour d'expérience.

Conception d'un système de télé-information EDF

Magazine
Marque
GNU/Linux Magazine
Numéro
197
Mois de parution
octobre 2016
Spécialité(s)
Résumé

La télé-information peut permettre, outre la surveillance de la consommation électrique, de piloter par exemple le système de chauffage (pompe à chaleur/chaudière fuel) en fonction des différentes périodes tarifaires EDF. Nous envisagerons de placer ce projet dans un cadre domotique général d'informatique répartie et d'insister sur la conception et la réalisation logicielle, notamment à l'aide de diagrammes de type UML.

Modélisation d'un système de téléinformation EDF

Magazine
Marque
GNU/Linux Magazine
Numéro
196
Mois de parution
septembre 2016
Spécialité(s)
Résumé

Le sujet a certes déjà été traité à plusieurs reprises dans la littérature informatique ; nous envisagerons ici de replacer ce projet dans un cadre domotique plus général d'informatique répartie et d'insister sur la conception et la réalisation logicielle, notamment à l'aide de diagrammes. En particulier la téléinformation peut permettre, outre la surveillance de la consommation électrique, de piloter par exemple le système de chauffage (pompe à chaleur / chaudière fuel) en fonction des différentes périodes tarifaires EDF.

Les derniers articles Premiums

Les derniers articles Premium

Bénéficiez de statistiques de fréquentations web légères et respectueuses avec Plausible Analytics

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Pour être visible sur le Web, un site est indispensable, cela va de soi. Mais il est impossible d’en évaluer le succès, ni celui de ses améliorations, sans établir de statistiques de fréquentation : combien de visiteurs ? Combien de pages consultées ? Quel temps passé ? Comment savoir si le nouveau design plaît réellement ? Autant de questions auxquelles Plausible se propose de répondre.

Quarkus : applications Java pour conteneurs

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Initié par Red Hat, il y a quelques années le projet Quarkus a pris son envol et en est désormais à sa troisième version majeure. Il propose un cadre d’exécution pour une application de Java radicalement différente, où son exécution ultra optimisée en fait un parfait candidat pour le déploiement sur des conteneurs tels que ceux de Docker ou Podman. Quarkus va même encore plus loin, en permettant de transformer l’application Java en un exécutable natif ! Voici une rapide introduction, par la pratique, à cet incroyable framework, qui nous offrira l’opportunité d’illustrer également sa facilité de prise en main.

Les listes de lecture

7 article(s) - ajoutée le 01/07/2020
La SDR permet désormais de toucher du doigt un domaine qui était jusqu'alors inaccessible : la réception et l'interprétation de signaux venus de l'espace. Découvrez ici différentes techniques utilisables, de la plus simple à la plus avancée...
8 article(s) - ajoutée le 01/07/2020
Au-delà de l'aspect nostalgique, le rétrocomputing est l'opportunité unique de renouer avec les concepts de base dans leur plus simple expression. Vous trouverez ici quelques-unes des technologies qui ont fait de l'informatique ce qu'elle est aujourd'hui.
9 article(s) - ajoutée le 01/07/2020
S'initier à la SDR est une activité financièrement très accessible, mais devant l'offre matérielle il est parfois difficile de faire ses premiers pas. Découvrez ici les options à votre disposition et les bases pour aborder cette thématique sereinement.
Voir les 31 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous