Migration du server/client domotique de “Select” vers “twisted”

Le serveur domotique hébergé sur mon RaspberryPi déclenche certaines actions en fonction d’évènements. Par exemple lorsqu’ un détecteur de présence est déclenché on allume la lumière de la même pièce. Dans cet exemple l’évènement sera reçût par le port USB du RaspberryPi (car il proviendra de l’Arduino Leonardo). Il existe d’autre évènements qui arriveront sur le RaspberryPi par d’autre moyen (TCP pour le site web de commande par exemple).

Jusqu’ a présent le serveur été code en python et attendait les évènements grâce au module python « select » qui me permettait de monitorer a la fois la liaison USB et TCP. Avec ce modèle je ne pouvais cependant pas faire du processing en arrière-plan (déclencher un évènement a une certaine heure par exemple) puisque le « select » est bloquant. J’ai donc choisit de migrer mon code vers Twisted qui correspond exactement a ce que je veux faire « Twisted is an event-driven networking engine written in Python and licensed under the open source »

La documentation est très bien faite est disponible sur le site officiel : http://twistedmatrix.com/trac/

Dans mon cas je veux que twisted monitor 2 « récepteurs » :

  • La liaison USB qui communique avec l’Arduino Leonardo est reçoit donc les messages des capteurs (capteur présence, détection incendie) qui peuvent être trigger à tout moment. Cette liaison est aussi utilisée pour envoyer des ordres aux capteurs/actionneurs du réseau (par exemple pour allumer la lumière)
  • La liaison TCP qui communique avec un client python fortement couple a un script PHP qui reçoit les ordres de la GUI HTML. Cette liaison est utilisée dans les 2 sens (le serveur répond au client qui affiche la réponse sur la page web).

La création de ses 2 « handlers » se fait assez facilement à condition de trouver le bon protocole duquel il faut hériter. Pour la liaison TCP il faut hériter de « twisted.internet.protocol. Protocol » (il existe déjà pas mal d’exemple pour ce cas de figure). Pour la liaison USB avec L’Arduino Leonardo on doit hériter de « twisted.protocols.basic. LineReceiver ». Ce protocole permet de recevoir les messages du port USB à condition qu’il soit bien envoyé comme une ligne complète. Il ne faut pas se tromper avec un autre protocole sinon les données seront tronquées.

Chacun de ses « handlers » a une méthode qui sera appelé lors de la réception de message : « lineReceived » pour l USB et « dataReceived » pour le TCP. Voilà à quoi ressemblent les 2 handlers

class UsbHandler(LineReceiver):
    """protocol handling class for USB """

    def __init__(self,iBrain,iRegisteredDevice):
        self.brain = iBrain
        self.registeredDevices = iRegisteredDevice

    def lineReceived(self, line):
        logging.info("USB Handler created to process : " + str(line))
        self.brain.HandleUsbInput(line,self.registeredDevices)

class TcpHandler(Protocol):
    """protocol handling class for TCP """

    def __init__(self,iBrain,iRegisteredDevice):
        self.brain = iBrain
        self.registeredDevices = iRegisteredDevice

    def dataReceived(self, data):
        logging.info("Tcp Handler created to process : " + str(data))
        if "READ" in str(data):
            logging.info("READ command")
            aRest = aBrain.ReadDeviceStatus2(data,aRegisterDevices)
            logging.info("READ command res " + str(aRest))
            self.transport.write(str(aRest))
        elif str(data) == "STOP": 
            logging.info("STOP command")
            aBrain.stop()
            reactor.stop()
        else:
            logging.info("Write command")
            aBrain.SendMessage(data,aRegisterDevices)
            self.transport.write("ACK")

 

Ensuite on attache ses 2 protocoles au “reacteur” qui est le Cœur de twisted. Pour le TCP on lui ajoute une surcouche au travers une factory (je ne sais pas pkoi il ne faut pas faire la même chose pour l USB…)

reactor.listenTCP(50007, TcpHandlerFactory(aBrain,aRegisterDevices))
SerialPort(UsbHandler(aBrain,aRegisterDevices), '/dev/ttyACM0', reactor, 9600)

 

Avec ce design je me retrouve dans la même configuration que celle de mon “framework – Select” fait maison. On va donc ajouter une boucle qui sera capable de gérer la logique background (celle qui ne dépend pas d’un évènement particulier).

Pour cela on créer une méthode (dans notre cas elle se contente d’appeler la vrai méthode de processing) :

def tired_task(iBrain):
    #logging.info("I want to run slowly" + str (datetime.datetime.now()))
    iBrain.smartProcessing2(aRegisterDevices)

et on l ajoute au “reactor” avec une frequence :

lc2 = LoopingCall(tired_task, aBrain)
 lc2.start(5)

Dans mon cas cette méthode va mettre a jour les capteurs automatiquement quand nécessaire. Par exemple si le dernier relevé de température date de plus de 5 minutes dans le salon et que la config de ce capteur autorise un relevé toutes les 5 minutes….on le met à jour. Cette méthode va également vérifier l’ensemble des capteurs stable de l’appartement et les updater si nécessaire. Par exemple on va éteindre la lampe de l’entrée si personne n’a été détecté depuis 10 minutes.

    def smartProcessing2(self,iListOfDevice):
        '''Une boucle qui a lieu regulierement pour prendre des decision. Elle va verifier les detecteur et en fonction triger certains evenements.
        Par ex si qq un est detecte dans l entree on decide d allumer la lumiere.
        Cette classe va aussi verifier si certaines autres actions (non lie a la detection de personne) peuvent etre prise.
        Par ex : si on a pas eut de detection de personne depuis un moment ds l entree et que la lumiere est allume....on etient
        Enfin elle va aussi mettre a jour tous les capteurs en fonction de leur refresh rate
        Par exemple si ca fait trop longtemps qu on a pas updater la T alors on la met a jour'''
        logging.info("Begining of a smart loop")

        #Step 1 : Verifier tous les detecteurs (interupteurs stables) pour voir si ils ont ete actives et prendre les actions correspondantes avant de les reset
        #Par exemple si le detecteur de fumee a ete active alors on va envoyer un mail 
        logging.info("Checking all possible event")
        for aOneDevice in iListOfDevice.registeredDevices:
            logging.debug("checking event : " + str(aOneDevice.id))
            if ((aOneDevice.id == 2) and (aOneDevice.currentStatus=="unstable")):
                sendEmailFireDetected()
            elif ((aOneDevice.id == 10) and (aOneDevice.currentStatus=="unstable")):
                self.PeopleDetectedEntree(iListOfDevice)
            elif ((aOneDevice.id == 9) and (aOneDevice.currentStatus=="unstable")):
                self.PeopleDetectedCharlesRoom(iListOfDevice)
            aOneDevice.reset()

        #Setp 2 : On reset les actions resultantes des detections passe
        #Par exemple si la lumiere de l entree ete ON car qq un avait ete detecte depuis 10 minutes mais qu il y a plus eu de detection depuis 10 min....on eteind
        logging.info("Reseting all previous automatic actions")
        for aOneDevice in iListOfDevice.registeredDevices:
            logging.debug("checking states : " + str(aOneDevice.id))
            if ((aOneDevice.id == 9) and ((iListOfDevice.getDevice(3)).currentStatus=="on") and (datetime.datetime.now() - aOneDevice.LastTMeaureDate > datetime.timedelta (seconds = 600))):
                self.TurnCharlesLightOff(iListOfDevice)
            elif ((aOneDevice.id == 10) and ((iListOfDevice.getDevice(8)).currentStatus=="on") and (datetime.datetime.now() - aOneDevice.LastTMeaureDate > datetime.timedelta (seconds = 180))):
                self.TurnEntreeLightOff(iListOfDevice)

        #Setp 3 : On force un refresh des capteurs periodiques
        logging.info("Force the auto refresh of capteur")
        for aOneDevice in iListOfDevice.registeredDevices:
            logging.debug("checking autoupdate : " + str(aOneDevice.id))
            if ( (aOneDevice.stateCanBeRefresh == True) and (aOneDevice.refreshOngoing == False)and (datetime.datetime.now() - aOneDevice.LastTMeaureDate > datetime.timedelta (minutes = aOneDevice.refreshRatemin) ) ):
                logging.debug("We can refresh : " + str(aOneDevice.id))
                self.refreshCapteur(aOneDevice,iListOfDevice)

Grace à cette nouvelle boucle intelligente la mise à jour des capteurs ne se fait plus dans la crontab avec :

# toutes les heures + 7 min on refresh la emperature entree
 7 * * * * /home/pi/Usb_Arduino_Leonardo/PythonWrapperWebArduinoUsbS.py -o CRONTAB -s 30

La mise à jour des capteurs fait maintenant partie intégrante de l’application.

J’ai également supprimé la base de données qui été interrogée par le site web et remplit par le serveur. Maintenant le site web interroge le serveur pour récupérer l’état T de n’importe quel capteur (en utilisant le format JSON).

case "CMD_READ" :
 $aCommandToExecute = WRAPPER2 . "-o " . getenv(REMOTE_ADDR) . " -s " . $_REQUEST["iCmdToExecute"] . " -t READ";
 $output = array();
 exec($aCommandToExecute, $output);
 print(json_encode($output));
 break;

renvoit :

[“‘{\”py\/object\”: \”Deipara_Objects.CapteurMesure\”, \”InPossibleCmd\”: {\”15\”: \”recoit Nouvelle T\”}, \”physicalLocation\”: \”\”, \”ActionsCommands\”: {\”15\”: \”self.currentStatus=aData\\\\nself.LastTMeaureDate=datetime.datetime.now()\\\\nself.refreshOngoing = False\”}, \”OutPossibleCmd\”: {\”15\”: \”recoit Nouvelle T\”}, \”porteuse\”: \”GATEWAY\”, \”stateCanBeRefresh\”: true, \”type\”: \”CapteurMesure\”, \”LastRefreshDate\”: {\”py\/repr\”: \”datetime\/datetime.datetime(2013, 4, 7, 22, 13, 48, 269822)\”}, \”refreshRatemin\”: 3, \”refreshOngoing\”: false, \”id\”: 15, \”currentStatus\”: 209.0, \”Reset\”: \”\”, \”LastTMeaureDate\”: {\”py\/repr\”: \”datetime\/datetime.datetime(2013, 4, 8, 15, 31, 38, 381700)\”}, \”PossibleStates\”: {}, \”description\”: \”\”}'”]

Au site Web et les informations nécessaires pour la page seront utilisés. Toute la logique est en train de migrer vers le serveur python pour qu’il coordonne l’ensemble des actions. Je clarifie également toutes les interfaces pour utiliser des formats standard (JSON plutôt que des select de colonnes dans une base).

L’ensemble du code est toujours dispo ICI