Programme d’arrosage automatique – version 0.1

Chalut tout le monde! Bien, alors comme je le disais dans mon dernier article à ce sujet, c’est bien beau d’avoir un système d’arrosage automatique dans ce petit potager, mais c’est toujours un peu galère de devoir tous les jours penser à activer l’arrosage avec ma fameuse petite appli android, et puis surtout, à lancer à chrono de quelques minutes en même temps et ne pas oublier de couper l’arrosage une fois que le temps est écoulé!

Avec un système pareil un jour ça va m’exploser à la tête cette histoire et je vais oublier de couper l’arrosage et on va se retrouvé inondés et avec une facture d’eau monstrueuse… [donc en prime je vais me faire déchirer ma face par biche 🤣]… Bref, vraiment très dangeureux tout ça alors, il est grand temps de s’offrir une petite upgrade 😉 !

L’idée de base…

Alors l’idée est plutôt simple je dirais: j’ai déjà un petit serveur Raspberry sur lequel tourne en permanence un serveur web en python (nous appellerons cette appli « homectrl » par la suite) avec lequel je peux communiquer pour activer/désactiver un relais qui contrôle l’électrovanne pour l’arrosage. Ce qu’il me faudrait dans un premier temps, ça serait d’avoir tout simple un thread dédié dans le process de cet serveur web qui va s’occuper de démarrer/arrêter l’arrosage tous les jours à certaines heures fixes (et je pourrais configurer ces horaires dans la config JSON globale de l’app par exemple).

Mais pour commencer, je dois rectifier le lancement de l’app homectrl: pour le moment j’utilise une version stockée directement sur le raspberry mais étant donné que je vais devoir faire pas mal de changement, je vais essayer de lancer tout ça depuis un partage samba sur mon serveur principal (neptune): ça sera plus pratique pour travailler efficacement!

Lancement depuis le partage array1

Donc on commence par préparer un nouveau fichier homectrl.sh prenant en compte ce nouveau chemin pour l’appli (et je le place cette fois ci avec mes autres fichiers de scripts pour cron [dans NervSeed/scripts/cron Manu… juste pour le cas ou un jour tu t’en souviendrais plus… 😂]

#!/bin/bash

lfile="/mnt/array1/admin/logs/homectrl.log"
appdir="/mnt/array1/dev/projects/NervSeed/python/apps/homectrl/"
app=${appdir}/homectrl.py

# check if we have a file called "restart_homectrl"
if [ -f "${appdir}/restart_homectrl" ]; then
  echo "Killing all homectrl processes..." >>$lfile 2>&1
  sudo pkill -f "${app}" >>$lfile 2>&1

  # Remove the request file:
  rm "${appdir}/restart_homectrl"
fi

nump=$(ps -ef | grep ${app} | grep -c -v grep)
# echo "Num processes: ${nump}"

if [ $nump -eq 0 ]; then
  echo "Starting homectrl process.">>$lfile 2>&1
  sudo python3 ${app} >>$lfile 2>&1
  echo "homectrl process done." >>$lfile 2>&1
fi

Ensuite, bien sûr on commente la ligne de commande de démarrage automatique de l’appli en local dans notre crontab, et on kill le process déjà en cours d’exécution:

$ crontab -e

# On commente cette ligne:
# * * * * * bash /home/pi/shared/apps/gatectrl/run.sh 2>&1 1>>/home/pi/shared/apps/gatectrl/homectrl.log 

$ sudo pkill -f "/home/pi/shared/apps/gatectrl/homectrl.py"

Et pour finir il suffit d’ajouter une nouvelle entrée à notre crontab, cette fois pour appeler notre fichier run.sh directement sur le partage CIFS:

$ crontab -e

# On ajoute cette ligne:
* * * * * bash /mnt/array1/dev/projects/NervSeed/scripts/cron/homectrl.sh

Après on attend juste une petite minute… et si tout se passe bien on devrait avoir un nouveau fichier de log « homectrl.log » avec quelques lignes dedans… hmmm… [3 heures et 28 minutes plus tard… 😁 Nan, je déconne hein: je suis pas totalement à l’ouest non plus quand même 😉 enfin… je crois pas ?…] Mais bien évidemment, y’a un truc qui à foiré en route on dirait 🤣! Allez, je débugue… Ah oui, effectivement mes redirections étaient incorrectes, gros béta que je suis! Mais c’est bon maintenant et j’ai des outputs dans homectrl.log comme prévu:

Starting homectrl process.
 * Serving Flask app "homectrl" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://0.0.0.0:80/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 282-787-311

Alors, oui oui, je sais, y’a bien marqué « Do not use it in a production deployment. »: mais bon, on est basiquement 4 personnes (voire juste 1.5 en fait…) à utiliser ce mini serveur, alors ça devrait tenir le coup! [d’ailleurs ça n’a jamais planté jusqu’à autant que je m’en souvienne…] Donc on va laisser ça comme ça pour le moment.

Et donc voilà, ça c’était pour les préliminaires. Maintenant il va falloir entrer dans le vif du sujet: écrire le code pour gérer l’arrosage.

Thread dédié pour l’arrosage

Alors, comme toujours, je commence avec une version minimale, qui ne fait… rien en fait 😉 Juste afficher un message pour dire qu’il faudrait faire quelque chose toute les 60 secondes environ:

from threading import *
from nv.core.log_utils import *

class WateringThread(Thread):
    def __init__(self, delay=60.0):
        Thread.__init__(self)
        
        self.delay = delay
        self.stopFlag = Event()
        self.start()

    def run(self):
        logDEBUG('Starting watering thread')
        while not self.stopFlag.is_set():
            logDEBUG("Should do something here.")
            self.stopFlag.wait(self.delay)

        logDEBUG('Watering thread ended.')

    def stop(self):
        self.stopFlag.set()
        self.join()

Puis je crée une instance de ce thread (qui démarre directement comme on peut le noter dans le code ci-dessus) dans le script principal homectrl.py, et effectivement, dans le fichier de log je commence à recevoir des petits messages toutes les minutes:

 * Detected change in '/mnt/array1/dev/projects/NervSeed/python/apps/homectrl/homectrl.py', reloading
 * Restarting with stat
2021-06-11T23:12:03.562747 [DEBUG] Starting homectrl server.
2021-06-11T23:12:03.583500 [DEBUG] Starting watering thread
2021-06-11T23:12:03.590141 [DEBUG] Should do something here.
 * Debugger is active!
 * Debugger PIN: 282-787-311
2021-06-11T23:13:03.596911 [DEBUG] Should do something here.
2021-06-11T23:14:03.598773 [DEBUG] Should do something here.
2021-06-11T23:15:03.600547 [DEBUG] Should do something here.
2021-06-11T23:16:03.602462 [DEBUG] Should do something here.

Et pour une fois… ça marche!

Après on utilise nos petits doigts magiques… tic tic tic tic… et bam! voili voilou! J’ai un petit programme qui fonctionne pour gérer l’arrosage de mon petit potager! Ben voilà, c’était pas si compliqué non ? 😁

from threading import *
from nv.core.utils import *
from datetime import datetime


class WateringThread(Thread):
    def __init__(self, gpioManager, delay=60.0):
        Thread.__init__(self)
        
        self.gman = gpioManager
        self.delay = delay
        self.stopFlag = Event()
        self.start()
        self.configFile = nvGetNervSeedPath("config/homectrl.json")
        self.relayName = "vgw"

        CHECK(not self.gman.isEnabled(self.relayName), "VGW pin should not be active here.")

    def run(self):
        logDEBUG('Starting watering thread')
        while not self.stopFlag.is_set():
            # Read the config file:
            cfg = nvLoadJSON(self.configFile)['watering']

            # get the default duration:
            dur = cfg['default_duration']

            # get all the start times:
            schedule=cfg['schedule']

            now = datetime.now()

            # Build a reference start time stamp:
            curTs = now.hour*3600 + now.minute*60 + now.second

            activate = False
            # Check each start time to see if we should activate watering at the current time:
            for startStr in schedule:
                els = startStr.split(":")
                # logDEBUG("Read start time elements: %s" % repr(els))
                ts = int(els[0])*3600 + int(els[1])*60
                if curTs >= ts and curTs <= (ts+dur):
                    # We should activate watering here::
                    activate = True
                    break
            
            # Now we request watering if needed:
            curActive = self.gman.isEnabled(self.relayName)
            verbose = cfg['verbose']
            if activate and not curActive:
                if verbose:
                    nvSendRocketChatMessage("ℹ Activating automatic watering.")
                logDEBUG("Activating automatic watering.")
            if not activate and curActive:
                if verbose:
                    nvSendRocketChatMessage("ℹ Desactivating automatic watering.")
                logDEBUG("Desactivating automatic watering.")

            # logDEBUG("Watering is %s" % ("ON" if activate else "OFF"))

            self.gman.enable(self.relayName, activate)
 
            self.stopFlag.wait(self.delay)

        logDEBUG('Watering thread ended.')

    def stop(self):
        self.stopFlag.set()
        self.join()

Alors, quelques petites explications concernant le code ci-dessus: le principe est simple: chaque minute, on va lire notre config JSON (comme ça je peux la modifier en live sans souci), et puis on va vérifier si on doit activer l’arrosage ou le désactiver. Ensuite j’utilise une classe dédiée (GPIOManager) pour gérer les intéractions de bas niveau avec le GPIO du raspberry:

import RPi.GPIO as GPIO

class GPIOManager(object):
    def __init__(self):
        GPIO.setmode(GPIO.BCM)
        GPIO.setwarnings(False)
        
        self.outPins = {
            "gate": 14,
            "vgw": 15
        }

        self.states = {
            "gate": False,
            "vgw": False
        }

        for k,pin in self.outPins.items():
            GPIO.setup(pin, GPIO.OUT)

        # turn relays OFF: 
        for k,pin in self.outPins.items():
            GPIO.output(pin, GPIO.LOW)
        
    def enable(self,key,val=True):
        if self.isEnabled(key) == val:
            # Nothing to change here.
            return 

        GPIO.output(self.outPins[key], GPIO.HIGH if val else GPIO.LOW)
        self.states[key] = val

    def disable(self, key):
        self.enable(key, False)

    def toggle(self, key):
        self.enable(key, not self.isEnabled(key))

    def isEnabled(self, key):
        return self.states[key]

    def isDisabled(self, key):
        return not self.isEnabled(key)

Au niveau de la config, ça reste vraiment très simple aussi pour commencer: j’active l’arrosage à chaque fois pour 8 minutes pour le moment (8*60 = 480), et je fournis une liste de temps auxquels je veux démarrer l’arrosage. Avec ça je peux calcule facilement chaque plage horaire pendant laquelle le relais doit être activé. Et ça marche sans problème pour l’instant 👌

{
  "watering": {
    "default_duration": 480,
    "verbose": true,
    "schedule": ["8:30", "12:30", "18:30"]
  }
}

Ah, et puis aussi, j’oubliais: j’aime bien aussi m’envoyer un petit message automatique sur notre serveur rocketchat perso, au moins pour les premiers jours pour suivre plus facilement si tout ce passe comme prévu depuis mon tel hi hi hi. D’où l’utilité de la fonction nvSendRocketChatMessage("ℹ Activating automatic watering.") dans le code ci-dessus.

Evidemment, comme je viens tout juste de terminer cette première version, j’ai déjà ma tête qui « se barre dans tous les sens » avec pleins d’idées pour les améliorations que je pourrais ajouter:

  • Afficher l’état du relais sur mon appli de contrôle
  • Gérer des « arrosages ponctuels » demandés depuis l’appli (si j’ai besoin d’arroser un truc manuellement par exemple)
  • Gérer le débitmètre installé sur le circuit pour mesurer la consommation d’eau,
  • Lier tout ça aux capteurs de température du système de chauffage solaire,
  • Stocker les statistiques récupérées dans une petite base de donnée sqlite
  • Permettre l’affichage sous forme de graphique sur l’app de contrôle des sessions d’arrosage + consommation, etc.
  • Unifier le fichier de config pour l’arrosage et la gestion du portail,

Bref… si je m’écoutais, je crois bien que je ne m’arrêterais jamais 😆 Mais on va quand même dire que ça suffit pour cette fois! @++ les gens!

One Comment

  1. Pingback:Chauffage solaire avec Raspberry Pi – NervTech's Blog

Leave a Comment

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