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 :

Schéma
Schéma

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.

IMGP1127

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 :

Vue extérieur 2
Vue exterieur 1
raspberry
Vue intérieur 2
Vue intérieur 1

Voici une capture d’écran de l’interface Web :

iweb

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.

Auteur: Maxime Maucourant

24 “Automatiser un poulailler avec une RaspBerry Pi

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

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

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

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

  3. 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 !

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

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

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

  6. 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é

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

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

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

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

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

  10. 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 ?

Laisser un commentaire

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