Arduino, Raspberry, NodeJS… De la Domotique en boite !

Lorsque j’ai commencé à explorer l’électronique et la domotique, mon objectif principal était surtout de pouvoir contrôler mes lumières, mais avec un moyen simple à déterminer. L’idée était de ne pas bricoler quelque chose qui nécessite plus de 5 secondes à actionner, voire de pouvoir l’utiliser avec HomeKit et Google Home via IFTTT. Et peu importe dans quelle direction je me tournais, tout revenait à la capacité qu’a une installation (un Raspberry Pi) d’interpréter une requête web, puis de l’acheminer vers un contrôleur (un Arduino Nano) qui lui-même va actionner un émetteur radio ou une led infrarouge. Et pour ça, on a besoin d’une app NodeJS !

 

Cet article fait partie d’une série en plusieurs parties :

Partie 1 : Lire et émettre un signal radio 433MHz
Partie 2 : Lire un signal infrarouge
Partie 3 : Emettre un signal infrarouge
Partie 4 : Communication entre un Raspberry Pi et un Arduino
> Partie 5 : Intégration via NodeJS, configuration du système complet de domotique

 

Hypothèses de départ

Lecture des précédents articles de la série

Cet article faisant partie d’une série de plusieurs billets traitant de la façon de lire un signal radio ou infrarouge, puis de les renvoyer, etc, je présume à ce point-ci que ces sujets sont connus, et que tout à été monté : L’Arduino sait commander les lumières, et sait communiquer avec le Raspberry Pi.

Connaissance de NodeJS

Cet article suppose également que vous avez déjà joué avec NodeJS, et que faire une petite application simple est faisable. Si ce n’est pas le cas, alors avant d’aller plus loins je vous recommande la lecture de ce cours d’OpenClassRooms: https://openclassrooms.com/courses/des-applications-ultra-rapides-avec-node-js

Redirection de ports sur le routeur

On va développer ensemble un serveur web NodeJS. Ce genre de serveur nécessitent qu’un port lui soit attribué. Je vais partir du principe que la configuration au niveau du routeur a déjà été faite, et que toutes les communications sur ce port sont redirigées vers le Raspberry Pi.

 

Architecture simplifiée

Je peux résumer mon choix d’architecture en me reposant sur ce que nous avons appris à faire sur les précédents articles, en particulier le billet détaillant la Communication entre un Raspberry Pi et un Arduino. On y parle notamment de la façon dont un programme sur Arduino peut lire le port série (simplifions en qualifiant « port série » de port USB) les caractères envoyés par un programme sur Raspberry Pi par USB (ici), et faire une action en conséquence.

En d’autres termes, cela revient à avoir un serveur NodeJS qui va, à la réception d’une requête GET, envoyer à l’Arduino via un script Python un caractère. L’Arduino saura alors le comprendre comme signifiant « allume la lumière du salon », ou « monte le son de la TV ».

Le code sur l’Arduino

Une fusion des précédents articles permet de mettre au point un programme simple, qui va surveiller la console série pour des caractères envoyés depuis le Raspberry, puis en fonction de ce caractère effectuer une action donnée.

Voilà en version raccourcie ce que ça donne :

#include <RCSwitch.h>
#include <IRLib.h>
IRsend My_Sender;
RCSwitch mySwitch = RCSwitch();
int message = 0;
void setup()
{
  Serial.begin(9600);
  // Transmitter is connected to Arduino Pin #10
  mySwitch.enableTransmit(10);
  // Optional set pulse length.
   mySwitch.setPulseLength(185);
  // Optional set number of transmission repetitions.
   mySwitch.setRepeatTransmit(8);
}
void loop()
{
  while (Serial.available())  {
    message = Serial.read()-'0';  // on soustrait le caractère 0, qui vaut 48 en ASCII
    Serial.println(message);
    switch(message)
    {
      //LUMIERES
      case 49: // a = Lampe du salon on
        mySwitch.send("000100010101010100110011");
      break;
      // [...]
      case 72: // x = Plafond Salle a manger off
        mySwitch.send("010100010011011010000100");
      break;
      //TELECOMMANDE TV
      case 59: // k = On/Off
        My_Sender.send(NEC,0x20DF10EF, 32);
      break;
      // [...]
      case 65: // q = Enter
        My_Sender.send(NEC,0x20DF22DD, 32);
      break;
      //TELECOMMANDE HOMECINEMA
      case 66: // r = Vol up
        My_Sender.send(SONY,0x240A, 15);
      break;
      // [...]
      case 68: // t = On/Off
        My_Sender.send(SONY,0x540A, 15);
      break;
      default:
        Serial.println("Default");
    }
  }
  delay(10);
}

 

L’application NodeJS

 

Disclaimer : Je suis peut-être ingénieur informatique de formation, mais je ne touche plus à du code pour autre chose que mes hobbies depuis presque 10 ans. De fait, je suis conscient que le code du serveur NodeJS va probablement faire s’arracher les yeux de tous les développeurs de ce monde, même amateurs, qui liront cet article. Je suis également conscient qu’il y a de la place à l’amélioration et que je n’ai peut-être pas fait les meilleurs choix. Aussi, si vous voyez quelque chose de flagrant à améliorer ou à optimiser, n’hésitez pas à me faire progresser 🙂

 

L’Arduino parle fourchelang, utilisons un python!

Nous avons besoin d’un script Python pour parler à l’Arduino, et on va l’appeler « Remote.py ». Ce script va juste lire le paramètre qui lui est passé, puis va l’envoyer dans la console série de l’Arduino. Voir l’article sur la Communication entre un Raspberry Pi et un Arduino pour plus de détails :

# coding=utf-8
import sys     # pour la gestion des parametres
import serial  # bibliothèque permettant la communication série
import time    # pour le délai d'attente entre les messages
ser = serial.Serial('/dev/ttyUSB0', 9600)
i=0
while i < 7:
    ser.write('0')
    ser.write(sys.argv[1])
    time.sleep(0.1)
    i += 1

 

Maintenant, vous allez dire « cool, comment on execute le script python depuis l’application NodeJS » ? Avec une fonction javascript qu’on va nommer, avec originalité, executePythonScript. Comme ceci :

var pyScripts = require('path').resolve(__dirname, 'scripts');
function executePythonScript(msg){
    var options = {scriptPath:pyScripts, args:[ commands[msg] ]};
    PythonShell.run('Remote.py', options, function (err) {
      if (err){ throw err; }
    });
}

 

Les données à manipuler

La première étape consiste à déterminer de quelles commandes j’ai besoin pour contrôler mes appareils. Je veux pouvoir allumer et éteindre plusieurs lampes ou interrupteurs (J’ai fait l’acquisition d’interrupteurs contrôlables par radio, idem pour quelques prises etekcity sur lesquelles sont branchées des lampes). Je veux également simuler certains boutons des télécommandes du home cinéma et de la TV (pour changer la source, contrôler le son, etc).

Cela donne, en utilisant des noms « explicites » auxquels on peut associer une lettre à envoyer à l’Arduino, le tableau de commandes suivant :

var commands = {
  "salonon" : "a", "salonoff" : "b", "samon" : "c", "samoff" : "d",
  "cuison" : "e", "cuisoff" : "f", "allon" : "i", "alloff" : "j",
  "tvonoff" : "k", "tvsource" : "l", "tvtop" : "m","tvright" : "n",
  "tvdown" : "o", "tvleft" : "p", "tventer" : "q",
  "hcvolup" : "r", "hcvoldown" : "s", "hconoff" : "t",
  "plafsalonon" : "u", "plafsalonoff" : "v",
  "plafsamon" : "w", "plafsamoff" : "x"
}

 

Pour cette application, je voulais être capable de sauvegarder l’état de chacune de mes lumières, après qu’elles aient été commandées. Pour une raison simple : Si j’envisage d’utiliser HomeKit, je dois pouvoir afficher à l’écran l’état d’une lumière. Sinon, j’aurais en permanence tous les boutons comme éteins.

On a donc notre second élément : un tableau d’états qui pour chaque type d’appareil commandé peut sauvegarder 0 ou 1 :

var states = {
  "sal" : '0',
  "sam" : '0',
  "cuis" : '0',
  "tvsal" : '0',
  "hcsal" : '0',
  "psal" : '0',
  "psam" : '0'
}

 

Enfin, il nous faut un tableau qui permette de faire le lien entre les deux. Par là je veux dire un tableau capable de me donner un nom d’appareil à commander et dont je dois changer l’état, qu’on soit en train d’essayer de l’allumer (ON) ou de l’éteindre (OFF) :

var commandsToState = {
  "salonon" : "sal", "salonoff" : "sal",
   "samon" : "sam", "samoff" : "sam",
  "cuison" : "cuis", "cuisoff" : "cuis",
  "tvonoff" : "tvsal", "hconoff" : "hc",
  "plafsalonon" : "psal", "plafsalonoff" : "psal",
  "plafsamon" : "psam", "plafsamoff" : "psam",
}

 

POST et GET sont sur un bateau…

Je le disais en introduction, je voulais pouvoir commander les lumières et autres appareils par des moyens simples. Pour être prêt à tout, je veux pouvoir réagir à une requête POST aussi bien qu’à une requête GET. La différence entre les deux, c’est qu’une requête GET ne peux pas avoir de paramètres « encapsulés », tout se passe dans l’url et ce n’est pas très propre. Un autre argument, c’est l’utilisation de homebridge qui fait le lien avec HomeKit.

Homebridge tel que je l’ai configuré ne permet de gérer que des requêtes GET. Or, pour sécuriser tant bien que mal l’accès à l’écosystème de domotique depuis l’extérieur (pour par exemple allumer les lumières quand j’arrive près de la maison, ou les éteindre quand je pars), j’ai voulu intégrer l’idée de « clé » (un bien grand mot mais je n’ai rien de mieux ^^) : Une publique dans l’url d’accès pour les requêtes GET et POST, l’autre privée dans les paramètres d’une requête POST.

Imaginons donc les deux clefs :

  • Publique : 123456789
  • Privés : 987654321

 

Requête GET

L’accessoire de HomeBridge, homebridge-http, a besoin de trois adresses à interroger : une pour « ON », une pour « OFF », et une pour consulter l’état de l’appareil. Dans ce cas, pas de problème : faisons trois requêtes. En analysant l’url, on va aller chercher le premier wildcard disponible. Ensuite, s’il s’agit d’une commande ON ou OFF, on va executer le script Python de tout à l’heure en lui passant le wildcard en paramètre, sans oublier de mettre le statut à jour. S’il s’agit d’une demande de statut, alors on retourne juste le statut correspondant :

//Homekit only : Get status
app.get('/123456789/status/*', function(req, res){
    res.send('' + states[req.params[0]])
});
//Homekit only : Set lights on
app.get('/123456789/on/*', function(req, res){
    executePythonScript(req.params[0]);
    states[commandsToState[req.params[0]]] = '1';
    res.send('on');
});
//Homekit only : Set light off
app.get('/123456789/off/*', function(req, res){
    executePythonScript(req.params[0]);
    states[commandsToState[req.params[0]]] = '0';
    res.send('off');
});

 

Requête POST

Quant à la requête POST, l’idée est d’avoir dans les entêtes de la requête une série de messages, qui sont en fait les commandes à executer. À la réception d’une requête POST sur l’adresse /123456789, on regarde les valeurs passées en paramètre en sachant que le premier d’entre eux sera la « clé privée » (soit 987654321).

Par exemple :

  • j’appelle http://[ddns].myddns.me:xxxx/123456789
  • avec comme content-type application/x-www-form-urlencoded
  • et comme valeurs MyPrivateKey=987654321&message=salonoff&message2=plafsalonoff

Ensuite, on va boucler sur ces messages en commençant au 2e, et pour chacun à partir du 2ème on appelle le script Python comme argument la lettre correspondant au code de la commande. J’ai fait le choix de me limiter à un maximum de 5 commandes possibles simultanées par requête POST. C’est comme ça, pas de raison particulière 🙂

Ca donne ceci :

app.post('/123456789', function (req, res) {
  var body="";
  req.on('data', function(chunk) { body+=chunk; });
  req.on('end', function () {
    var bodyReq = body.split("&"); //Séparation des paramètres
    var keyReq = bodyReq[0].split("=")[1]; // La clef de la requete POST
    var messageReqArr = [];
    messageReqArr[0] = bodyReq[1].split("=")[1]; // Le message, action a effectuer
    //Testons voir si il y a d'autres messages
    if(bodyReq[2]){ messageReqArr[1] = bodyReq[2].split("=")[1]; }
    if(bodyReq[3]){ messageReqArr[2] = bodyReq[3].split("=")[1]; }
    if(bodyReq[4]){ messageReqArr[3] = bodyReq[4].split("=")[1]; }
    if(bodyReq[5]){ messageReqArr[4] = bodyReq[5].split("=")[1]; }
    //Bouclons sur tous les message reçus
    for (var i = 0, length = messageReqArr.length; i < length; i++) {
      messageReq = messageReqArr[i];
      //Testons si la commande existe,
      //et si la clé privée correspond à ce à quoi on s'attend
      if((messageReq in commands) && keyReq == "987654321"){
        //Si c'est une commande ON (dernière lettre N), on met à jour l'état
        if (messageReq.slice(-1) == "n"){
          states[commandsToState[messageReq]] = '1';
        }
        //Idem pour OFF
        else{
          states[commandsToState[messageReq]] = '0';
        }
        executePythonScript(messageReq);
      }
    }
    res.sendStatus(200);
  });
});

 

Si vous mettez tous les morceaux bout à bout, vous avez l’application NodeJS! Elle est capable de recevoir des requêtes GET et POST, et de communiquer avec un Arduino pour lui demander d’envoyer des impulsions radio ou infrarouge pour commander vos appareils.

 

Lancer l’application NodeJS comme un service

Maintenant, si on souhaite installer cette application (au hasard) sur un Raspberry Pi, il faut pouvoir aussi la lancer comme un service. Pourquoi? Parce que vous ne voulez pas être obligé de laisser une fenêtre de terminal ouverte avec l’application NodeJS démarrée. Vous ne voulez pas non plus être obligé de vous reconnecter en SSH au Raspberry Pi à chaque redémarrage de ce dernier. Pour ça, on va utiliser un truc super : forever and forever-service !

On va installer d’abord forever et forever-service sur le Raspberry :

npm install forever -g
npm install forever-service -g

Une fois que c’est fait, il faut « cd » son chemin vers le dossier de l’application, en prenant soin d’avoir le fichier principal nommé « app.js ». Puis, on crée le service qu’on appellera RemoteControl :

forever-service install RemoteControl

Si tout s’est bien passé, vous devriez pouvoir démarrer, arrêter ou redémarrer le service avec ces commandes :

service RemoteControl start
service RemoteControl stop
service RemoteControl restart

Maintenant, on veut lancer le service à chaque démarrage du Raspberry. Pour cela, il faut ouvrir le fichier /etc/rc.local :

sudo nano /etc/rc.local

Puis y insérer la ligne suivante :

sudo service RemoteControl start

 

Intégration de HomeKit

Sans aller trop loin dans l’utilisation de HomeBridge (dont je me sers pour faire le lien entre le serveur NodeJS et HomeKit), je renverrais (lâchement) à la lecture du wiki de HomeBridge est très bien fait, je n’ai pas de valeur ajoutée à le réécrire ici si ce n’est le traduire en français.

Ce qu’il faut garder à l’esprit, c’est que la configuration de homebridge que j’ai choisie m’impose comme je le disais plus haut de pouvoir gérer des requêtes GET. Voici à quoi ressemblerait un accessoire utilisant la capacité de l’application NodeJS à interpréter une requête GET :

{
  "accessory": "Http",
  "name": "Lampe Salon",
  "switchHandling": "yes",
  "http_method": "GET",
  "on_url":      "http://192.168.0.XX:xxxx/123456789/on/salonon",
  "off_url":     "http://192.168.0.XX:xxxx/123456789/off/salonoff",
  "status_url":  "http://192.168.0.XX:xxxx/123456789/status/sal",
  "sendimmediately": ""
}

 

Intégration de Google Home via IFTTT

Google Home est capable de commander pas mal d’appareils de domotique, mais pas (directement) quelque chose de bricolé maison. En revanche, il sait très bien parler avec IFTTT, et ça c’est utile !

Si vous ne connaissez pas IFTTT, cela signifie If This Then That. C’est un service en ligne qui permet de générer des actions à partir de déclencheurs. Les déclencheurs peuvent être « il est telle heure », « un nouvel article est publié sur ce site », « mon GPS me détecte dans cette zone », « J’ai reçu un SMS », « Le babyphone détecte un bruit » … On peut imaginer beaucoup de choses puisqu’il existe plus de 360 partenaires qui fonctionnent avec IFTTT : https://ifttt.com/search/services. Lorsqu’un déclencheur est détecté (If This), on fait une action (Then That). Ils appellent la combinaison des deux des Applets.

Il existe donc entre autres une chaine « Google Assistant ». Il suffit d’y connecter le compte Google utilisé sur votre Google home et le tour est joué. On n’a plus qu’a configurer un « If telle phrase, Then telle requête web ». Voici quelques unes de mes recettes (applets):

Par exemple pour « Eteins les lumieres », IFTTT nous propose deux autres phrases alternatives, le choix de la langue, ainsi que la réponse attendue (par exemple « ok, j’éteins les lumières »).

L’action à effectuer en cas de détection de cette phrase, c’est un « WebHook » de type POST, à l’adresse « http://[ddns].myddns.me:xxxx/123456789 », le content-type « application/x-www-form-urlencoded » et comme body par exemple « MyPrivateKey=987654321&message=salonoff&message2=plafsalonoff ».

Le tour est joué !

Architecture finale

Finalement, en intégrant différent services, voici à quoi ressemble mon installation finale :

 

C’est dans la boîte !

Une fois qu’on a fait l’installation, il faut encore ranger le tout dans une belle boite décorative. J’ai rajouté un capteur de température et de luminosité :

 

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *