Notez que je décline toutes responsabilités quant aux conséquences que pourraient avoir l'application des méthodes et conseils suivants ainsi que l'utilisation des programmes présentés. Ceux-ci pourraient être erronés ou obsolètes.
Sur la raspberry pi, on propose d'installer et d'utiliser un serveur web léger lighttpd. Il fait parti des paquetages disponibles qu'il suffit d'installer.
lighty-enable-mod cgi systemctl restart lighttpd
On peut ajouter l'accès https avec un certificat. On pourrait faire l'acquisition d'un certificat auprès d'une autorité de certification, mais pour une utilisation sur un réseau local qui n'est pas accessible sur internet, on peut simplement utiliser un certificat auto-signé qui se crée, sous linux, avec la commande openssl comme par exemple
openssl req -sha256 -nodes -newkey rsa:2048 -keyout raspi.key -out raspi.csr openssl x509 -req -days 365 -in raspi.csr -signkey raspi.key -out raspi.crt cat raspi.key raspi.crt > /etc/lighttpd/server.pem
La première ligne crée les fichiers clé et certificat, la deuxième ligne crée la signature et enfin la troisième ligne crée le fichier certificat utilisé par le serveur et qui doit être dans le répertoire /etc/lighttpd/server.pem. Pendant l'étape de création, il faut répondre aux questions d'identification du propriétaire du site comme le nom, prénom, email, entreprise, ville, pays, ...
Pour comprendre exactement la commande openssl ou bien personnalisé et modifier les options, il faut se reporter à la documentation openssl.
Il faut également ajouter la configuration ssl au serveur lighttpd avec les commandes :
lighty-enable-mod ssl systemctl restart lighttpd
Il est possible de mettre à disposition des pages html pour chaque utilisateur, ces pages sont accessibles avec l'url http://ip/~utilisateur/. Pour cela on doit créer le répertoire public_html dans le répertoire utilisateur et ajouter la configuration au serveur
lighty-enable-mod userdir systemctl restart lighttpd
On propose de réaliser un premier script CGI qui permet de stocker les données des capteurs dans la base de donnée présentée dans le chapitre précédent. La sauvegarde concerne uniquement la table mesures. Ce script permet de stocker les valeurs de 5 capteurs : température,humidité,pression,luminosité,uv. La partie générique du script est inspirée du chapitre sur le web
Table objets
idobjet | nom | idcapteur |
---|---|---|
1 | température | 1 |
2 | humidité | 1 |
3 | pression | 2 |
4 | luminosité | 3 |
5 | UV | 4 |
Table capteurs
idcapteur | reference | designation | idconstructeur | idfournisseur |
---|---|---|---|---|
1 | HTS221 | température et humidité | 1 | 1 |
2 | LPS22HB | pression | 1 | 1 |
3 | TEMT6000X01 | luminosité | 2 | 1 |
4 | VEML6075 | uv | 2 | 1 |
Table constructeurs
idconstructeur | nom |
---|---|
1 | STMicroelectronics |
2 | Vishay Semiconductors |
Table fournisseurs
idfournisseur | nom |
---|---|
1 | Go Tronic |
Script CGI sauvecapteurs.cgi
#!/bin/bash
FICHIER="/var/sqlite/objetsconnectes.db"
SQLENTETE="insert into mesures (date,valeur,idobjet) values ("
if [ $REQUEST_METHOD = "GET" ]
then
CHAINE=$QUERY_STRING
else
read CHAINE
fi
temperature=""
humidite=""
pression=""
luminosite=""
uv=""
retour=5
LISTE=${CHAINE//&/ }
for param in $LISTE
do
champ=${param%=*}
valeur=${param#*=}
valeur=${valeur//+/ }
case "$champ" in
temperature)
temperature=$valeur
retour=$(( $retour - 1))
;;
humidite)
humidite=$valeur
retour=$(( $retour - 1))
;;
pression)
pression=$valeur
retour=$(( $retour - 1))
;;
luminosite)
luminosite=$valeur
retour=$(( $retour - 1))
;;
uv)
uv=$valeur
retour=$(( $retour - 1))
;;
esac
done
DATE=$(date +"%Y-%m-%d %H:%M:%S")
if [ -n "$temperature" ]
then
SQL="${SQLENTETE} \"${DATE}\" , ${temperature} , 1 );"
sqlite3 "${FICHIER}" "${SQL}"
retour=$(($retour - $?))
fi
if [ -n "$humidite" ]
then
SQL="${SQLENTETE} \"${DATE}\" , ${humidite} , 2 );"
sqlite3 "${FICHIER}" "${SQL}"
retour=$(($retour - $?))
fi
if [ -n "$pression" ]
then
SQL="${SQLENTETE} \"${DATE}\" , ${pression} , 3 );"
sqlite3 "${FICHIER}" "${SQL}"
retour=$(($retour - $?))
fi
if [ -n "$luminosite" ]
then
SQL="${SQLENTETE} \"${DATE}\" , ${luminosite} , 4 );"
sqlite3 "${FICHIER}" "${SQL}"
retour=$(($retour - $?))
fi
if [ -n "$uv" ]
then
SQL="${SQLENTETE} \"${DATE}\" , ${uv} , 5 );"
sqlite3 "${FICHIER}" "${SQL}"
retour=$(($retour - $?))
fi
cat << EOF
Content-type: text/plain
code=$retour
EOF
L'URL d'envoi des données est de la forme
https://ip/cgi-bin/sauvecapteurs.cgi?temperature=23.5&humidite=50&pression=1013&luminosite=800&uv=1
Le fichier de la base de données SQLite n'est pas accessible via http, car il se trouve en dehors de la racine du site.
La valeur de retour est initialisée à 5, ce qui fait que si tous les paramètres ont été transmis, elle vaut 0 en fin de boucle.
Après la boucle de traitement des paramètres, on prend la date système afin d'horodater les données reçues. La date est commune aux 5 données enregistrées.
Ensuite on sauvegarde chaque donnée dans la base dans l'ordre suivant : température,humidité,pression,luminosité,uv. On associe à chaque valeur l'identifiant correspondant à la référence de la donnée de la table objets.
Enfin le code retour vaut 0 si tout s'est bien passé, si un problème de stockage est rencontré, on aura une valeur de retour négative.
En résumé, une valeur de retour positive indique une erreur de transmission de paramètres et une erreur de retour négative indique une erreur de stockage de données.
On propose de réaliser un script qui affiche l'ensemble des mesures dans une page html.
Script CGI litcapteurs.cgi
#!/bin/bash
FICHIER="/var/sqlite/objetsconnectes.db"
cat << EOF
Content-type: text/html
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="/~michel/css/presentation.css">
<title>Affichage des capteurs</title>
</head>
<body>
<h1 align="center">Affichage des capteurs</h1>
EOF
LISTEBRUTEOBJETS=$(sqlite3 ${FICHIER} "select idobjet from mesures group by idobjet")
LISTEOBJETS=$(echo "$LISTEBRUTEOBJETS" | tr "\012" " ")
TOTALRANGEES=$(sqlite3 ${FICHIER} "select count(idmesure) from mesures")
TOTALOBJETS=$( echo "$LISTEBRUTEOBJETS" | grep -c "^[0-9]\+$")
TOTALMESURES=$( expr ${TOTALRANGEES} / ${TOTALOBJETS})
RESTE=$( expr ${TOTALRANGEES} % ${TOTALOBJETS})
echo "<p align=\"center\">$TOTALMESURES mesures</p>"
if [ "$RESTE" != 0 ]
then
echo "<p>Attention Absence de mesures ou mesures erronnées</p>"
fi
ENTETEHTML=""
for idobjet in $LISTEOBJETS
do
ENTETE=$(sqlite3 ${FICHIER} "select nom from objets where idobjet=${idobjet}")
ENTETEHTML="$ENTETEHTML<th>${ENTETE}</th>"
done
echo "<table width=\"100%\">"
echo "<tr>$ENTETEHTML</tr>"
rangee=0
LISTEMESURES=$( seq 1 $TOTALMESURES)
for mesure in $LISTEMESURES
do
COLONNE=$(sqlite3 ${FICHIER} "select valeur from mesures limit ${rangee},${TOTALOBJETS}")
LISTEBRUTE=$(echo "$COLONNE" | tr "\012" ",")
LISTECOLONNE=$( expr "$LISTEBRUTE" : "\(.*\),$")
RANGEEHTML=$(echo "$LISTECOLONNE" | sed -e 's/,/<\/td><td>/g' )
RANGEEHTML="<tr><td align=\"center\">${RANGEEHTML}</td></tr>"
echo "$RANGEEHTML"
rangee=$(( $rangee + ${TOTALOBJETS}))
done
echo "</table>"
cat << EOF2
</body>
</html>
EOF2
Ce script construit un tableau HTML à raison d'un ensemble de mesures par ligne à partir d'une table où chaque ligne correspond à une valeur. Il faut effectuer la lecture par paquet de 5 lignes afin d'avoir les 5 valeurs à chaque lecture. Ensuite il faut afficher les 5 valeurs dans une ligne HTML.
Pour cela on a besoin du nombre de rangées (TOTALRANGEES) dans le tableau mesures. On a également besoin de la liste des objets présents dans le tableau mesures (LISTEOBJETS) ainsi que le nombre d'objets différents (TOTALOBJETS). Ce qui permet de déduire le nombre de mesures (TOTALMESURES).
Le calcul du reste permet de vérifier si le nombre de rangées est bien un multiple du nombre d'objets.
On construit l'entête du tableau à partir de la liste des objets (LISTEOBJETS) en utilisant le tableau objets qui contient les désignation des capteurs.
Maintenant pour chaque mesure, on lit l'ensemble des rangées du tableau correspondant aux valeurs des capteurs. On obtient donc une colonne de valeurs, qu'il faut transformer en ligne en remplaçant le caractère de fine de ligne (012 en octal) en une virgule. Puis on supprime la dernière virgule avant de construire la ligne HTML en remplaçant la virgule par des balises de tableau.
Le déplacement dans la base de donnée se fait tous les (TOTALOBJETS).
La page HTML utilise la feuille de style présentée dans le chapitre sur le web.
Exemple d'affichage de la page HTML
Le serveur MQTT utilisé se nomme mosquitto, il fait parti des paquetages de la raspberry et peut donc être installé facilement.
Il faut également installer le client pour pouvoir tester le serveur : mosquitto_pub pour la publication de messages et mosquitto_sub pour l'abonnement.
Par défaut le serveur n'accepte pas les connexions anonymes, Il est possible modifier la configuration en ajoutant un fichier de configuration dans le répertoire /etc/mosquitto/conf.d :
listener 1883 0.0.0.0 alloow anonymous true
On ouvre deux fenêtres shell une pour lancer l'abonnement, l'autre pour effectuer la publication.
On utilise la librairie de développement de mosquitto. Il faut inclure le fichier de définitions mosquitto.h et ajouter mosquitto à l'édition de lien.
L'essentiel des fonctions est :
mosquitto_lib_init()
pour initialiser la librairiemosquitto_new()
pour créer un nouvel objet client mosquitto et retourne un pointeur vers un objet mosquittomosquitto_connect(objetmosquitto,ip,port,timeout)
établit la connexion avec le serveur, le timeout est un temps maximal d'attente de réponse.mosquitto_publish(objetmosquitto,suivi,topic,longueurmessage,message,qos,retain)
avec suivi qui est un pointeur vers un entier qui contient un indicateur suivi de message et peut être NULL, qos est la qualité de service qui peut être 0 et retain est le maintien de message qui peut être false.mosquitto_subscribe(objetmosquitto,suivi,topic,qos)
avec suivi qui peut être également NULLmosquitto_loop_start(objetmosquitto)
pour démarrer la boucle de surveillance de l'abonnement, doit être suivi par l'attente de l'appui sur une touche du clavier.mosquitto_loop_stop(objetmosquitto,arret)
arrête la boucle de surveillance avec arret, booléen, qui vaut vrai pour forcer l'arrêt du thread.mosquitto_disconnect(objetmosquitto)
termine la connexion avec le serveurmosquitto_destroy(objetmosquitto)
libère l'objet mosquittocode pqtt_pub.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <mosquitto.h>
#define HOSTNAME "courtier"
#define PORT 1883
#define TOPIC "/maison/piece/lumiere"
int main(int argc, char **argv) {
struct mosquitto *mqtt_client = NULL;
char message[64];
int err;
if (argc == 2) {
strcpy(message,argv[1]);
}
else {
strcpy(message,"allume");
}
int lgmessage = strlen(message);
err = mosquitto_lib_init();
if (err != MOSQ_ERR_SUCCESS) {
fprintf(stderr,"ERREUR init mosquitto\n");
return EXIT_FAILURE;
}
mqtt_client = mosquitto_new("mqttclient", true, NULL);
err = mosquitto_connect(mqtt_client, HOSTNAME,PORT, 60);
if(err != 0){
fprintf(stderr,"erreur connexion : %s\n", strerror(err));
mosquitto_destroy(mqtt_client);
mosquitto_lib_cleanup();
return EXIT_FAILURE;
}
mosquitto_publish(mqtt_client, NULL, TOPIC,lgmessage , message , 0, false);
mosquitto_disconnect(mqtt_client);
mosquitto_destroy(mqtt_client);
mosquitto_lib_cleanup();
return EXIT_SUCCESS;
}
Le programme publie le message transmis en paramètre au topic /maison/piece/lumiere, la valeur par défaut est allume. Ce programme respecte les étapes d'initialisation, connexion, publication, déconnexion et suppression de l'objet client et libération de la mémoire.
En cas d'erreur de connexion, l'objet client est supprimé, la mémoire allouée est libérée avant de terminer le programme sur un code d'erreur.
La publication se fait avec une qos de 0 et sans maintien du message.
code mqtt_sub.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <mosquitto.h>
#define HOSTNAME "courtier"
#define PORT 1883
#define TOPIC "/maison/piece/lumiere"
#define CONNECT_KEEP_ALIVE 30
void on_message(struct mosquitto *mosq, void *obj, const struct mosquitto_message *msg) {
printf("message %s : %s\n", msg->topic, (char *) msg->payload);
}
int main(int argc, char **argv) {
struct mosquitto *mqtt_sub = NULL;
int err = mosquitto_lib_init();
if (err != MOSQ_ERR_SUCCESS) {
fprintf(stderr,"ERREUR init mosquitto\n");
return EXIT_FAILURE;
}
mqtt_sub = mosquitto_new(NULL, true, NULL);
if (mqtt_sub == NULL) {
fprintf(stderr,"ERREUR new mosquitto\n");
return EXIT_FAILURE;
}
mosquitto_message_callback_set(mqtt_sub, on_message);
err = mosquitto_connect(mqtt_sub, HOSTNAME,PORT, CONNECT_KEEP_ALIVE);
if(err != 0){
fprintf(stderr,"erreur connexion : %s\n", strerror(err));
mosquitto_destroy(mqtt_sub);
mosquitto_lib_cleanup();
return EXIT_FAILURE;
}
mosquitto_subscribe(mqtt_sub, NULL, TOPIC, 0);
mosquitto_loop_start(mqtt_sub);
printf("Appuyer sur une touche pour terminer ...\n");
getchar();
mosquitto_loop_stop(mqtt_sub, true);
mosquitto_disconnect(mqtt_sub);
mosquitto_destroy(mqtt_sub);
mosquitto_lib_cleanup();
return EXIT_SUCCESS;
}
Le programme s'abonne au topic /maison/piece/lumiere. Ce programme respecte les étapes d'initialisation, connexion, souscription, boucle de surveillance, arrêt de la boucle de surveillance, déconnexion et suppression de l'objet client et libération de la mémoire.
En cas d'erreur de connexion, l'objet client est supprimé, la mémoire allouée est libérée avant de terminer le programme sur un code d'erreur.
On utilise la librairie python paho. Ici on utilise les connexions simples pour la publication avec le paquetage paho.mqtt.publish et l'abonnement avec le paquetage paho.mqtt.suscribe, ou encore un abonnement permanent avec fonction callback
publish.single(topic,message,hostname)
publie un message vers le serveur hostname (cela peut être l'adresse ip). suscribe.simple(topic,hostname)
s'abonne au serveur pour le topic, cette fonction bloque jusqu'à réception d'un message. Cette fonction retourne un objet qui contient le topic et le message.suscribe.callback(fonctioncallback,topic,hostname)
s'abonne au serveur pour le topic, cette fonction boucle indéfiniment, la focntion fonctioncallback est appelée à chaque réception de message.code mqtt_pub.py
import paho.mqtt.publish as publish
hostname = "courtier"
topic = "/maison/piece/lumiere"
def main(args):
nbargs=len(args)
if nbargs==2 :
message = args[1]
else :
message = "allume"
publish.single(topic, message, hostname=hostname)
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
Le message est transmis en argument. En l'absence d'argument, c'est le message par défaut allume qui est publié.
code mqtt_sub.py
import paho.mqtt.subscribe as subscribe
hostname = "courtier"
topic = "/maison/piece/lumiere"
def main(args):
message = subscribe.simple(topic, hostname=hostname)
print(f"topic {message.topic} : {message.payload}")
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
La fonction effectue la souscription, et bloque jusqu'à l'arrivée d'un message.
code mqtt_sub.py
import paho.mqtt.subscribe as subscribe
hostname = "courtier"
topic = "/maison/piece/lumiere"
def messagerecu(client, userdata, message):
print(f"topic {message.topic} : {message.payload}")
def main(args):
subscribe.callback(messagerecu, topic, hostname=hostname)
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
La fonction effectue la souscription puis lance une boucle de surveillance. A chaque message la fonction messagerecu est appelée pour traiter le message reçu.
L'installation de domoticz est expliquée sur le site de l'application domoticz pour raspberry pi ou encore l'installation sous linux.
Attention : l'installation présentée sur ce site configure le mode https avec le port 443, utilisé par les serveurs web comme lighttpd.
Pour une utilisation occasionnelle ou des tests avant installation définitive, on peut également l'installer dans le répertoire utilisateur pi (/home/pi) en mode utilisateur. Dans ce cas il faut configurer le numéro de port https dans l'espace utilisateur, comme cela est expliqué dans le chapitre précédent. On peut utiliser le script de démarrage suivant :
#!/bin/sh ${HOME}/domoticz/domoticz -sslwww 6443 -sslcert ${HOME}/domoticz/server_cert.pem
Après avoir rendu ce script exécutable, il suffit de l'exécuter pour lancer le serveur domoticz, puis ctlr-c pour stopper le serveur.
Télécharger l'archive pour raspberry pi
Toutes les commandes se font avec sudo ou en root
tar zxvf domoticz_linux_armv7l.tgz
groupadd -r domoticz
useradd -r -g domoticz -b /opt -s /usr/sbin/nologin domoticz
chown -R domoticz:domoticz domoticz
USERNAME=domoticz DAEMON=/opt/domoticz/$NAME DAEMON_ARGS="-daemon" DAEMON_ARGS="$DAEMON_ARGS -daemonname $NAME -pidfile $PIDFILE" DAEMON_ARGS="$DAEMON_ARGS -www 8080" DAEMON_ARGS="$DAEMON_ARGS -sslwww 6443 -sslcert /opt/domoticz/server_cert.pem"ou appliquer le patch domoticz.patch :
patch < domoticz.patch
sudo -u domoticz -g domoticz /opt/domoticz/domoticz -sslwww 6443 -sslcert /opt/domoticz/server_cert.pemSi le démarrage est OK, se connecter avec un navigateur pour vérifier que tout fonctionne :
https://ip:6443/
cp domoticz.sh /etc/init.d/domoticz
chmod a+x domoticz
./domoticz startSi le démarrage est OK, se connecter avec un navigateur pour vérifier que tout fonctionne :
https://ip:6443/
update-rc.d domoticz.sh defaults
reboot
Lors de la première connexion avec le login admin et le mot de passe domoticz, dans le menu Settings->Parameters ,il faut choisir la langue et fournir obligatoirement des coordonnées GPS comme par exemple une latitude de 48.862725 et une longitude : 2.287592 (place du Trocadéro à Paris).
Ensuite il faut créer un utilisateur, qui sera utilisé pour les connexions des objets connectés, mais également pour visualiser et gérer les données des capteurs, le mode administrateur étant réservé à la l'administration. Dans le menu Configuration->Users, il faut saisir un nom (ex user) et un mot de passe (ex access) ainsi que les droits utilisateur, activer les menus qui seront utilisés (Interrupteurs, Température, Météo, Mesures ) puis cliquer sur Ajouter.
Eventuellement, dans le menu Configuration->Paramètres->sécurité activer la protection API et ajouter les réseaux de confiance.
On propose d'ajouter les composants température, Humidité, Pression, luminosité, et indice UV.
La syntaxe des commandes à utiliser par les objets connectés pour mettre à jour les valeurs des capteurs est disponible dans la documentation du site domoticz
https://user:access@courtier:6443/json.htm?type=command¶m=udevice&idx=1&nvalue=0&svalue=valeurvaleur est la valeur de la température.
curl "http://courtier:8080/json.htm?type=command¶m=udevice&idx=2&nvalue=valeur&svalue=statut"valeur est la valeur de l'humidité et statut est compris entre 0 et un indicateur compris entre 0 et 3 (normal, confortable, sec, pluvieux )
https://user:access@courtier:6443/json.htm?type=command¶m=udevice&idx=3&nvalue=0&svalue=valeur;previsionvaleur est la valeur de la pression, prevision est la prévision entre 0 et 6 (stable, ensoleillé, nuageux, instable, orageux, inconnu, pluie).
https://user:access@courtier:6443/json.htm?type=command¶m=udevice&idx=4&svalue=valeurvaleur est la valeur de la luminosité.
https://user:access@courtier:6443/json.htm?type=command¶m=udevice&idx=5&nvalue=0&svalue=valeur;temperaturevaleur est la valeur de l'indice UV, temperature n'est pas utilisé et doit être à 0.
Ajouter un sélecteur de niveau en allant dans le menu Interrupteurs, cliquer sur Ajout manuel. Choisir un nom (ex led RGB) et le type selector, on créer un interrupteur qui permet de choisir un niveau de commande, qui pourra, par exemple, piloter une led RGB sur un objet connecté. A cet effet, on pourra renommer les niveaux 1 à 3 en rouge,vert,bleu et ajouter le niveau blanc.
Cela consiste à ajouter un composant matériel qui est MQTT Client Gateway with LAN interface, qu'il faut configurer avec :
Avec cette configuration un clic sur un des niveaux (off,rouge,vert,bleu,blanc) publie le message json suivant :
{ "Battery" : 255, "LastUpdate" : "2024-07-15 13:26:47", "LevelActions" : "||||", "LevelNames" : "Off|rouge|vert|bleu|blanc", "LevelOffHidden" : "false", "RSSI" : 12, "SelectorStyle" : "0", "description" : "", "dtype" : "Light/Switch", "hwid" : "2", "id" : "00000000", "idx" : 6, "name" : "led RGB", "nvalue" : 2, "org_hwid" : "2", "stype" : "Selector Switch", "svalue1" : "20", "switchType" : "Selector", "unit" : 1 }
La valeur du champ svalue1 contient la valeur du niveau correspondant au bouton (ici 20 pour rouge).