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 lorsque qu’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 cameras, 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.

A 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 à é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.