Automatiser un poulailler avec une RaspBerry Pi
Pourquoi ce projet ?
Il m’est venu par un ami, pour qui fermer chaque soir et ouvrir chaque matin le poulailler devenait une corvée surtout lorsqu’il devait s’absenter de la maison pendant quelques jours. Il serait dommage qu’un renard passe par là au moment où vous n’avez pas pu le fermer. Ainsi, j’ai décidé d’automatiser le système d’ouverture et de fermeture avec une RaspBerry Pi et une carte d’extension PiFace. Avec les nombreuses possibilités que m’offre cette carte, j’ai pu ajouter deux webcams : une permettant d’observer les mouvements des poules, l’autre permettant de vérifier la ponte des œufs ainsi qu’une lampe. Le tout est contrôlable à partir d’une interface web (Responsive Design) qui permet une multitude d’actions : ouvrir/fermer la porte, allumer/éteindre la lumière, visionner les caméras, voir les informations système et modifier à tout moment la programmation horaire de la porte et de la lumière.
Matériels utilisés:
- 1 RaspBerry Pi (39,95 € chez Domadoo)
- 1 carte d’extension PiFace (36,90 € chez Domadoo)
- 3 interrupteurs de fin de course (récupération)
- 1 moteur de type essuie-glace (récupération)
- 1 jeu de poulie + crémaillère (récupération)
- 1 lampe 230V
- 1 transformateur 230/12V à courant continu 2A (récupération)
- 1 transformateur de type chargeur pour téléphone USB (récupération)
- 2 relais 5V (récupération)
- 2 webcams (récupération)
- 1 paire de CPL ePlug E200 (28 € chez ElectroDépot)
Voici le schéma de branchement de l’extension PiFace :
Les deux relais présents sur la carte permettent de faire une inversion des pôles afin de faire tourner le moteur dans les deux sens de rotation. Cette solution nécessite un relais (R1) intermédiaire permettant de contrôler l’alimentation du moteur. Le relais R2 quant à lui permet de gérer l’allumage de la lampe.
À quoi servent les interrupteurs I1, I2 et I3 ?
L’interrupteur I1 est un fin de course, il est enclenché par la porte lorsque celle-ci est ouverte a contrario de I2 qui lui est enclenché lorsque la porte est fermé.
L’interrupteur I3 est une sécurité dite anti-écrasement. Imaginez la scène. Vous avez lancé une fermeture de la porte mais une poule décide de sortir pendant la fermeture de celle-ci, et là c’est le drame : la poule se fait briser les os par la porte. Afin d’éviter cela, j’ai décidé de monter une partie mobile munie d’un ressort souple qui actionne l’interrupteur I3 (voir le schéma ci-dessous) permettant de faire une remontée immédiate de la porte.
En cas de dysfonctionnement des deux interrupteurs de fin de course et pour éviter une surchauffe du moteur, une temporisation lors de la levée et de la descente a été ajoutée dans le programme. Dans mon cas, j’ai calculé qu’il fallait environ 7 secondes pour la fermeture et 9 secondes pour l’ouverture. Si ce délai est dépassé, alors l’alimentation du moteur se coupe instantanément et cela est accompagné d’une notification par email.
Installation des scripts python permettant de contrôler PiFace.
sudo apt-get install python3-pifacedigitalio
Pour tester son bon fonctionnement, vous pouvez essayer les scripts présents dans le répertoire suivant.
cd /usr/share/doc/python3-pifacedigitalio/examples
Allons un peu plus loin !
Pour pouvoir contrôler l’ensemble, j’ai écrit un script dans le langage Python. Il est conçu pour pouvoir être utilisé au travers d’une API GET avec retour formaté JSON (JavaScript Objet Notation), ce qui le rend facilement exploitable avec une librairie JavaScript comme JQuery.
Exemples :
Obtenir le statut de la porte et de la lumière : http://ip:8000/?all=status
Retourne :
{ "door" : "Closed", "light" : "OFF" }
Obtenir les informations du système : http://ip:8000/?system=infos
Retourne :
{ "cpu" : { "used" : "50.0", "temp" : "53.0" }, "memory" : { "used" : "246.5", "total" : "437.7", "free" : "191.2" }, "disk" : { "used" : "1.1", "total" : "3.6", "free" : "2.3", "perc" : "67"; } }
En cas de problème avec la porte (problème d’ouverture/fermeture, sécurité anti-écrasement), un email est automatiquement envoyé.
Un journal avec les différentes criticités nommé webcontroller.log est également disponible dans le dossier /var/log.
Voici le script complet, j’y ai ajouté des commentaires afin de faciliter sa compréhension :
#!/usr/bin/python3 ############################### ##Ecrit par Maxime MAUCOURANT## ##mmaucourant at free dot fr ## ############################### import sys import os import http.server import urllib.parse import pifacedigitalio import threading import time import socketserver import smtplib import logging #Paramètres du journal LOG_NAME = "webcontroller" LOG_PATH = "/var/log/webcontroller.log" LOG_FORMAT = "%(asctime)s -> %(levelname)s -> %(message)s" #Paramètres d'envoi de mail SMTP_ADDR = "smtp.free.fr" SMTP_PORT = 25 SMTP_DEBUGLEVEL = 0 SMTP_FROM = "poulailler@dom.ext" SMTP_TO = ["email1@dom.ext", "email2@dom.ext"] #Messages envoyés par mail (Objet, Message) MAIL_MESS = [ ["Fermeture de la porte", "Une erreur s'est produite lors de la fermeture de la porte !"], ["Securite activee !", "Un objet a bloque la fermeture de la porte !"], ["Ouverture de la porte", "Une erreur s'est produite lors de l'ouverture de la porte !"] ] #Paramètres du serveur WEB_PORT = 8000 WEB_ADDR = "127.0.0.1" #Sorties OUT_MOTOR_POWER = 7 OUT_LIGHT = 6 #Entrées IN_SECURITY_SENSOR = 5 IN_DOWN_SENSOR = 6 IN_UP_SENSOR = 7 #Délai de fermeture et d'ouverture x 100ms CLOSE_TIMER = 70 # x 100ms OPEN_TIMER = 90 # x 100ms #Procédure d'écriture dans le journal def log(message, level = 0): print(message) if level == 0: logger.setLevel(logging.INFO) logger.info(message) elif level == 1: logger.setLevel(logging.WARNING) logger.warning(message) elif level == 2: logger.setLevel(logging.ERROR) logger.exception(message) #Procédure d'envoi de mail def Sendmail(subject, message): try: server = smtplib.SMTP() server.set_debuglevel(SMTP_DEBUGLEVEL) server.connect(SMTP_ADDR, SMTP_PORT) server.sendmail(SMTP_FROM, SMTP_TO, """From: %s\nTo: %s\nSubject: %s\n%s""" % (SMTP_FROM, ", ".join(SMTP_TO), subject, message)) server.quit() log("Successfully sent email") except smtplib.SMTPException: log("Unable to send email", 1) #Thread de fermeture/ouverture de la porte class DoorAction(threading.Thread): #Procédure d'initialisation du thread def __init__(self, action, pfd, res): threading.Thread.__init__(self) self.action = action self.pfd = pfd self.response = res #Fonction de fermeture de la porte def close(self): i = 0 log("Close in process") #Boucle permettant de contrôler la descente toutes les 100ms afin d'éviter une surchauffe du moteur en cas de dysfonctionnement du fin de course bas while i < CLOSE_TIMER: #Si la porte n'est pas fermée if self.pfd.input_pins[IN_DOWN_SENSOR].value == 0: #Rotation horaire self.pfd.relays[0].value = 1 self.pfd.relays[1].value = 1 #Démarrage du moteur self.pfd.output_pins[OUT_MOTOR_POWER].value = 1 #Si le capteur de sécurité est enclenché if self.pfd.input_pins[IN_SECURITY_SENSOR].value == 1: #Ouverture de la porte self.open(True) return else: #Arrêt du moteur self.pfd.output_pins[OUT_MOTOR_POWER].value = 0 #Arrêt des relais permettant l'inversion self.pfd.relays[0].value = 0 self.pfd.relays[1].value = 0 log("Door closed") self.response("{\"door\": \"Closed\"}") return i += 1 #100ms time.sleep(0.1) #Arrêt du moteur self.pfd.output_pins[OUT_MOTOR_POWER].value = 0 log("Error when closing", 1) Sendmail(MAIL_MESS[0][0], MAIL_MESS[0][1]) self.response("{\"door\": \"Error when closing\"}") return #Fonction d'ouverture de la porte def open(self, security = False): i = 0 log("Open in process") #Boucle permettant de contrôler la montée toutes les 100ms afin d'éviter une surchauffe du moteur en cas de dysfonctionnement du fin de course haut while i < OPEN_TIMER: # x 100 ms #Si le capteur de fin de course haut n'est pas enclenché if self.pfd.input_pins[IN_UP_SENSOR].value == 0: #Rotation antihoraire self.pfd.relays[0].value = 0 self.pfd.relays[1].value = 0 #Démarrage du moteur self.pfd.output_pins[OUT_MOTOR_POWER].value = 1 else: #Si le capteur de sécurité n'a pas été enclenché if not security: log("Door opened") self.response("{\"door\": \"Opened\"}") #Capteur de sécurité enclenché else: log("Security activated", 1) Sendmail(MAIL_MESS[1][0], MAIL_MESS[1][1]) self.response("{\"door\": \"Security activated\"}") #Arrêt du moteur self.pfd.output_pins[OUT_MOTOR_POWER].value = 0 return i += 1 #100ms time.sleep(0.1) #Arrêt du moteur self.pfd.output_pins[OUT_MOTOR_POWER].value = 0 log("Error when opening", 1) Sendmail(MAIL_MESS[2][0], MAIL_MESS[2][1]) self.response("{\"door\": \"Error when opening\"}") return #Procédure appelée lors de l'instanciation du thread def run(self): if self.action == "close": self.close() if self.action == "open": self.open() #Classe de traitement des requêtes class WebHandler(http.server.BaseHTTPRequestHandler): #Procédure permettant de retourner une réponse formatée JSON au client def response(self, json): self.send_response(200) self.send_header("Content-type", "application/json") self.end_headers() self.wfile.write(bytes(json, "UTF-8")) #Fonction permettant d'obtenir le statut de la porte def getDoorStatus(self): if self.pfd.input_pins[IN_UP_SENSOR].value == 1: print("Door is open") return("Opened") if self.pfd.input_pins[IN_DOWN_SENSOR].value == 1: print("Door is close") return("Closed") return("Unknown") #Fonction permettant d'obtenir le statut de la lumière def getLightStatus(self): #Si la sortie est activée if self.pfd.output_pins[OUT_LIGHT].value == 0: print("Light is OFF") return("OFF") else: print("Light is ON") return("ON") return #Fonction d'obtention de la température du CPU def getCPUtemp(self): res = os.popen("vcgencmd measure_temp").readline() return(res.replace("temp=","").replace("'C\n","")) #Fonction d'obtention de l'espace mémoire 1: Total, 2: Utilisé, 3: Libre def getRAMinfo(self): p = os.popen('free') i = 0 while True: i += 1 line = p.readline() if i == 2: return(line.split()[1:4]) #Fonction d'obtention de la charge du CPU def getCPUusage(self): return(str(os.popen("top -b -n 5 -d 0.2 | grep \"Cpu\" | awk 'NR==3{ print($2)}'").readline().strip().replace(",", "."))) #Fonction d'obtention de l'espace disque 1: Total, 2: Utilisé, 3: Libre, 4: Pourcentage utilisé def getDiskSpace(self): p = os.popen("df -m /") i = 0 while True: i += 1 line = p.readline() if i == 2: return(line.split()[1:5]) #Fonction appelée lors d'une requête http def do_GET(self): try: #Extraction des paramètres qs = urllib.parse.urlparse(self.path).query query_components = urllib.parse.parse_qs(qs) #Informations du système if "system" in query_components: action = query_components["system"][0] #Informations sur le système if action == "infos": CPU_temp = self.getCPUtemp() CPU_used = self.getCPUusage() RAM_infos = self.getRAMinfo() DISK_infos = self.getDiskSpace() RAM_total = round(int(RAM_infos[0]) / 1024, 1) RAM_used = round(int(RAM_infos[1]) / 1024, 1) RAM_free = round(int(RAM_infos[2]) / 1024, 1) DISK_total = round(int(DISK_infos[0]) / 1000, 1) DISK_free = round(int(DISK_infos[1]) / 1000, 1) DISK_used = round(int(DISK_infos[2]) / 1000, 1) DISK_perc = DISK_infos[3][:-1] self.response(""" {{ "cpu\" : {{ \"used\" : \"{0}\", \"temp\" : \"{1}\" }}, \"memory\" : {{ \"used\" : \"{2}\", \"total\" : \"{3}\", \"free\" : \"{4}\" }}, \"disk\" : {{ \"used\" : \"{5}\", \"total\" : \"{6}\", \"free\" : \"{7}\", \"perc\" : \"{8}\" }} }}""".format(CPU_used, CPU_temp, RAM_used, RAM_total, RAM_free, DISK_used, DISK_total, DISK_free, DISK_perc)) return #Requêtes concernant la porte if "door" in query_components: action = query_components["door"][0] #Ouverture if action == "open": thread = DoorAction("open", self.pfd, self.response) thread.start() thread.join() return #Fermeture if action == "close": thread = DoorAction("close", self.pfd, self.response) thread.start() thread.join() return #Requêtes concernant la lumière if "light" in query_components: action = query_components["light"][0] #Allumage if action == "on": self.pfd.output_pins[OUT_LIGHT].value = 1 log("Light ON") self.response("{\"light\": \"ON\"}") return #Extinction if action == "off": self.pfd.output_pins[OUT_LIGHT].value = 0 log("Light OFF") self.response("{\"light\": \"OFF\"}") return #Requête concernant le statut if "all" in query_components: action = query_components["all"][0] #Statut de la porte et de la lumière if action == "status": door = self.getDoorStatus() light = self.getLightStatus() self.response(""" {{ \"door\" : \"{0}\", \"light\" : \"{1}\" }}""".format(door, light)) return #Requête invalide log("Invalid query", 1) self.response("{\"error\": \"Invalid query\"}") except Exception as ex: log(ex, 2) pass return class ThreadingServer(socketserver.ThreadingMixIn, http.server.HTTPServer): pass if __name__ == "__main__": #Initialisation du journal logger = logging.getLogger(LOG_NAME) hdlr = logging.FileHandler(LOG_PATH) formatter = logging.Formatter(LOG_FORMAT) hdlr.setFormatter(formatter) logger.addHandler(hdlr) #Initialisation de l'interface PiFace WebHandler.pfd = pifacedigitalio.PiFaceDigital() log("Starting web control") #Paramètres du serveur HTTP server_address = (WEB_ADDR, WEB_PORT) try: #Instanciation du serveur HTTP httpd = ThreadingServer(server_address, WebHandler) httpd.serve_forever() except KeyboardInterrupt: log("Shutting down server") httpd.socket.close() hdlr.close()
Pour que le script puisse se lancer au démarrage du système, il faut créer un script shell nommé webcontroller dans le dossier /etc/init.d/
Voici le contenu du script :
#!/bin/sh ### BEGIN INIT INFO # Provides: webcontroller # Required-Start: $remote_fs $syslog # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: # Description: ### END INIT INFO DIR=/usr/local/bin/webcontroller DAEMON=$DIR/webcontroller.py DAEMON_NAME=webcontroller DAEMON_USER=root PIDFILE=/var/run/$DAEMON_NAME.pid . /lib/lsb/init-functions do_start () { log_daemon_msg "Starting system $DAEMON_NAME daemon" start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON log_end_msg $? } do_stop () { log_daemon_msg "Stopping system $DAEMON_NAME daemon" start-stop-daemon --stop --pidfile $PIDFILE --retry 10 log_end_msg $? } case "$1" in start|stop) do_${1} ;; restart|reload|force-reload) do_stop do_start ;; status) status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $? ;; *) echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}" exit 1 ;; esac exit 0
Une fois le fichier crée, il faut lui donner la possibilité de s’exécuter avec la commande suivante :
chmod +x /etc/init.d/webcontroller
Création des liens symboliques dans chaque répertoire d’initialisation défini dans le script précédent :
update-rc.d webcontroller defaults
J’ai donc ensuite construit une interface web qui permet de contrôler l’ensemble du système de manière très simple et intuitive avec en prime des informations sur l’état du système ainsi que la météo qui, quant à elle, se base sur l’API de Yahoo.
Quelques photos de l’installation :
Voici une capture d’écran de l’interface Web :
Une petite vidéo de démonstration :).
Si ce projet vous intéresse n’hésitez pas à me contacter via la page Qui-suis-je ?.
Vous pouvez également télécharger l’image de la carte SD ici.
A bientôt.
Bonjour Maxime,
Je trouve ce projet très intéressant et proche de ce que je souhaite réaliser. Malheureusement j’ai moins de connaissance et j’aimerais en apprendre plus. Sans vouloir tout « pomper » j’aimerais toutefois voir le code global du système. Plus précisément de la page web. Serait-il possible de me transmettre ces informations.
Cordialement
Daniel
Bonjour Daniel,
La semaine prochaine, je vais mettre à disposition une image de la carte SD qui est sur la Raspberry Pi. Vous pourrez donc profiter pleinement de toutes les sources pour mener à bien votre projet.
Je vous tiens au courant.
Bonjour, votre mecanisme fait rever. Bravo. Ce qui m’interesserait de connaitre c’est comment, en fct de la luminosité, fermer et ouvrir la porte du poulailler. Comme cela doit être traité dans votre prototype, cette partie m’intéresse. Auriez vous la liste de tout ce qui nécessaire a ce type de realisation, ainsi que le cablage ? Cela pourrait etre un defi assez fun a relever. Merci.
Bonjour,
Merci ! Il n’y a pas de détecteur crépusculaire, cela fonctionne avec des horaires programmés via l’interface web. Pour la liste des éléments, ceux-ci sont indiqués sur l’article. A l’heure actuelle, possédez-vous déjà quelques éléments ?
Je reste à votre disposition pour tout renseignement.
Bonjour,
Très beau projet en effet et cela m’inspire déjà pour mon futur poulailler…
Toutefois, si la partie Python est très bien décrite, comme le fait remarquer Daniel, il serait intéressant de pouvoir voir le code web pour le comprendre et s’en inspirer. D’après la charte graphique, une partie me fait penser à Ez server monitor, est-ce le cas ? 🙂
Merci à toi en tout cas pour ce beau projet !
Bonjour Romain,
Merci beaucoup !
Je suis désolé pour le temps d’attente, j’ai ajouté un lien permettant de télécharger l’image de la carte SD en fin d’article.
Si tu as des questions, n’hésites pas. Tiens moi au courant de l’avancement de ton projet.
A très bientôt j’espère.
Bonjour Maxime,
Je suis toujours aussi curieux d’en apprendre plus. J’aimerais beaucoup voir comment tu a crée ton interface web.
Tu disais que tu mettrais le code bientôt.
Cordialement.
Daniel
Bonjour Daniel,
Tu peux télécharger l’image à la fin de l’article.
A bientôt.
Bonjour Maxime,
J’ai téléchargé l’image et je ne sais pas comment changer l’adresse IP, masque et passerelle.
merci je debute
cordialement
Sebastien
Bonjour Sébatien,
Une fois connecté en SSH il faut lancer la commande suivante : nano /etc/network/interfaces
Pour sauvegarder : CRTL + O
Pour quitter : CTRL + X
Puis : /etc/init.d/networking restart
Bonjour,
Le Raspberry ne se lance pas quand je met la carte SD avec l’image :/
Pour mon projet, j’aimerais juste avoir le code source de sélection d’horaire, malheureusement actuellement je suis bloqué
Bonjour Aurélien,
Quelle est la version de votre RaspBerry Pi. L’image ne boot que sur le version 2.
Bonjour,
Quel est le login ssh de l’image que tu as mis en ligne ?
Merci d’avance
Bonjour Dorian,
Les identifiants de connexion SSH sont dans l’archive ZIP.
Bonjour, votre projet est excellent ! J’aurais voulu avoir quelques informations supplémentaires sur la gestion calendaire et comment vous l’avez conçu, cordialement, Brice.
Bonjour Godin,
Je viens seulement de voir votre commentaire !
De quels informations avez-vous besoin ?
Bonjour
Quand j’écrit votre .iso sur ma carte sd ma raspberry ne boot pas , j’utilise win32 disk pour le faire avez vous fait le .iso sur un raspberry 2 ou 3 ? Car le mien est un 3.
Cordialement
Falvo
Bonjour Falvo,
Il faut un raspberry pi 2.
Cordialement
bonjour
Superbe réalisation pourvez vous me dire si cela est réalisable pour une personne qui y connais rien de rien a la RaspBerry Pi et a la domotique .Le seul point commun c est le poulaillé et de plus j ai pas le même système de fermeture ce n est pas une trappe mais une porte
Bonjour Chenal,
Merci beaucoup ! 🙂
Avez-vous un peu de connaissance en électricité ?
Il s’agit d’un modèle de poulailler acheté dans le commerce ?
Bonjour Maxime
j ai quelque notion d électricité et d électronique car j ai un BEP BAC PRO électronique
le poulailler n est pas acheter dans le commerce je l ai fini ce week-end
je partage des photos dès que possible
Mon but c est une fermeture crépusculaire mais également si cela est possible par site web ou application .
Bonjour Gérald,
Oui il est tout à fait possible d’ajouter un interrupteur crépusculaire, la PiFace comporte pas mal d’entrée. Il est également possible de contrôler le tout par application ou site web grâce à l’API.
Bonjour,
Tout d’abord bravo pour ce projet et les explications super travail ! Merci !
J’ai l’idée de réaliser la même chose. Je possède un raspberry pi3 model B et un moteur d’essuie glace 12V. Je comprend l’intérêt de la Piface pour commander le moteur mais j’ai une question sur les relais. En effet, les relais de la carte Piface on besoin en entrée 5V délivrer par Int/out du raspberry mais en sortie il sont dimensionner pour du 230V/10A non ?
Est-ce-qu’il s’agit d’une valeur max de fonctionnement ?
Est-ce qu’avec un transfo 12V qui alimente la carte Piface cela enclenche le relais ?
Une autre question sur l’alimentation du raspberry.
Faut-il 2 alimentations une de 5V pour le raspberry et une de 12V pour la carte Piface ?
Bonjour Remy,
Merci beaucoup :).
Oui vous pouvez contrôler du 230 V en 10 A sans problème.
Non l’alimentation de la carte PiFace ce fait via la Rasp.