Lib jeedomdaemon - délai du callback

Lib # jeedomdaemon 0.9.8

Bonjour @Mips ,

J’essaie d’intégrer la lib en référence dans mon plugin SomfyUnified.

Dans un premier temps, la mise en oeuvre est aisée et la procédure très bien décrite.
Merci pour ce beau travail.

Mais j’avoue que depuis quelques jours, je rame un peu car il y a quelque chose que je n’ai pas compris et je dois passer à coté de quelque chose ?

En quelques mots mais je pourrais développer si cela s’avère nécessaire:

  1. Lorsque je démarre le Démon du panneau du plugin, le Daemon ‹ start › comme attendu
  2. Au démarrage Daemon, je retourne le message 'Start à ma fonction de callback
  3. Puis je lance une boucle avec une période 1 seconde qui elle-même envoi un message ‹ tick › à la fonction callback

Ca ! C’est ce que je pensais obtenir.

Le hic dans tout ca, c’est que si je mets un délai dans la fonction de callback (qui pourrait simuler une durée de traitement, etc, …), ce délai est pris en compte par le Daemon avant sa prochaine action.
Et je prévois un traitement assez long dans mon appli !!!

Est-ce le fonctionnement attendu ?
Ais-je une coquille ou un oubli dans mon code ?

Je ne demande pas un debbugage de mon code en règle car je ne veux pas te prendre trop de temps. Juste un avis.

En espérant avoir été suffisament clair.
Et surtout, avec tous mes remerciements d’avance.

Je joint ci-après copie du code de mes 2 scripts et un screenshot:

  1. Le Daemon
# SomfyUnified
# @author Eridani78
#
# @version 0.0.0 2024 06 07
# 
# Description: Jeedom plugin Daemon user file
# File: somfyunifiedd.py

# ------------------------------------------------------------------------------

import asyncio

from jeedomdaemon.base_daemon import BaseDaemon
from jeedomdaemon.base_config import BaseConfig

# ------------------------------------------------------------------------------

class SUDaemonConfig(BaseConfig):
    """This is where you declare your custom argument/configuration

    Remember that all usual arguments are managed by the BaseConfig class already so you only have to take care of yours; e.g. user & password in this case
    """
    def __init__(self):
        super().__init__()

        self.add_argument("--textmessage", type=str, default='')
        self.add_argument("--daemontickperiod", type=float, default=1.0)

# ------------------------------------------------------------------------------

class SUDaemon(BaseDaemon):
    def __init__(self) -> None:
        # Standard initialisation
        super().__init__(config=SUDaemonConfig(), on_start_cb=self.on_start, on_message_cb=self.on_message, on_stop_cb=self.on_stop)

        # Add here any initialisation your daemon would need

    async def on_start(self):
        """
        This method will be called when your daemon start.
        This is the place where you should create yours tasks, login to remote system, etc
        """
        # if you don't have specific action to do on start, do not create this method

        self._logger.info("|Dm| ==> '%s'", self._config.textmessage)

        # Create a background task: fetch_events_loop
        self._logger.info("|Dm| Start fetch_events")
        self._fetch_events_task = asyncio.create_task(self._fetch_events_loop(self._config.daemontickperiod))

    async def on_message(self, message: list):
        """
        This function will be called once a message is received from Jeedom; check on api key is done already, just care about your logic
        You must implement the different actions that your daemon can handle.
        """
        self._logger.info("|Dm| ==> message['action'] '%s'", message['action'])

    async def on_stop(self):
        """
        This callback will be called when daemon need to stop`
        You need to close your remote connexions and cancel background tasks if any here.
        """

        self._fetch_events_task.cancel()
        await self.send_to_jeedom({'fetch_events':f'stop'})
        self._logger.info("|Dm| Stop fetch_events")

    async def _fetch_events_loop(self, tickperiod: float):
        """
        This is an implementation of a backgroudn task.
        You must have a try ... except asyncio.CancelledError: It will intercept the cancel request from the loop.
        """

        self._logger.info("|Dm| Start fetch_events_loop")
        await self.send_to_jeedom({'fetch_events':f'start'})
        try:
            while True:
                self._logger.info("|Dm| Loop with period %s sec",tickperiod )
                await self.send_to_jeedom({'fetch_events':f'tick'})
                await asyncio.sleep(tickperiod)
        except asyncio.CancelledError:
            self._logger.info("|Dm| Stop fetch_events_loop")

# ------------------------------------------------------------------------------
# START
# ------------------------------------------------------------------------------

SUDaemon().run()

# ------------------------------------------------------------------------------
# END
# ------------------------------------------------------------------------------

  1. La fonction de callback en test
<?php
/* This file is part of Jeedom.
 *
 * Jeedom is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Jeedom is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Jeedom. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * SomfyUnified
 * 
 * @author Eridani78
 * @version 1.0.0 2024 06 15
 *  
 * Description: Callback function to receive communication data from Daemon to PHP code.
 * 
 * File: jeeSomfyUnified.php
 */

/*     * ************************** use Declarations ************************ */
use SomfyUnified\UtilsLib;


/*     * **************************** Include Files ************************* */
require_once __DIR__ . "/../../../../core/php/core.inc.php";

/*     * ******************************************************************** */

try {
    // Import pluginId
    $pluginId = SomfyUnified::getPluginId();

    // Authenticate communication thru Jeedom API key verification
    if (!jeedom::apiAccess(init('apikey'), $pluginId)) {
        echo __('Access not granted ! ', __FILE__);
        exit();
    }

    if (init('test') != '') {
        echo 'OK';
        log::add($pluginId, 'info', 'test from daemon');
        die();
    }


    /** Read raw data from the request body ($dataString is a JSON formatted string) */
    $dataString         = file_get_contents("php://input");

    /** Process $dataString */
    if (is_null($dataString)) {
        log::add($pluginId, 'debug', __FILE__ . ' Data from Daemon is null. Failed to import Data.');
        throw new Exception(__FILE__ . ' Data from Daemon is null. Failed to import Data.');
    } elseif ($dataString === "") {
        $data = $dataString;
    } else {
        // Decode the JSON formatted string in an appropriate PHP type. JSON object is converted to array)
        $data = UtilsLib::decodeIfJson($dataString, null, 512, JSON_INVALID_UTF8_IGNORE | JSON_OBJECT_AS_ARRAY);
        if ($data === false) {
            log::add($pluginId, 'error', '|Je| $dataString (is NOT JSON)= ' . json_encode($dataString));
            //throw new Exception(__FILE__ . ' Data from Daemon imported but (Data is NOT JSON).');
        } else {
            //log::add($pluginId, 'debug', 'Succeeded to import Data from Daemon (Data is JSON).');
        }
    }
    log::add($pluginId, 'info', '|Je| Message from Daemon received in PHP (json_encode): ' . json_encode($data));


    /** Process action from Daemon */
    switch (true) {

        case ($data === ""):
            // Do nothing ("" is automatically send by SUDaemon and received when Daemon start)
            break;

        case ($data['fetch_events'] === 'start'):
            sleep(20);
            break;

        case ($data['fetch_events'] === 'tick'):
            sleep(10);

            break;

        case ($data['fetch_events'] === 'stop'):

            break;

    }

} catch (Exception $exception) {
    log::add($pluginId, 'error', '|Je| ' . displayException($exception));
}
  1. Un screenshot montrant que le tick n’est pas espacé d’une seconde mais de 10

Je regarde demain matin, plus le temps ce soir et faut que je regarde ça sur ordi

OK merci :+1:

Hello,

Si je peux me permettre quelques remarques ? :

Pourquoi ne pas utiliser directement la fonction json_decode ? comme cela par exemple :

$result = json_decode(file_get_contents("php://input"), true);
if (!is_array($result)) {
    die();
}

Cela t’evite tout le bloc pour tester si tu recois qqch, convertir en json, et ensuite tester l’array, si elle est vide, etc… :slight_smile:

Ensuite pour ta fonction fetch_events, je vois dans ton code php, tu fais dormir ton code pendant 10 sencodes, c’est pas pour ca que ca se déclenche que toutes les 10 secondes ?

D’ailleurs, c’est aussi pour ca qu’après ton start, le premier tick n’arrive que 20 secondes plus tard (entre 59:57 et 00:17) car tu fais dormir ton code 20 secondes après le start.

Je n’ai lu ton code que rapidement, mais c’est la première idée qui me vient, car côté démon tu initialise bien ta task avec le paramètre self._config.daemontickperiod qui j’imagine est à la valeur float de 1.0 seconde, donc de ce côté là, c’est good.

En enlevant les sleep(20) et sleep(10) de ton code, cela devrait être mieux, non ?

TiTidom.

Hello @TiTidom ,

Euhh ! Comment dire ?

Tout d’abord merci pour ton message et les conseils.

  1. Pour la première remarque, ta proposition de code est plus compacte mais en phase de développement, je préferre souvent détailler un peu pour m’y retrouver plus facilement quand je reviens sur mon code.

  2. Pour les sleep(), ils ne sont la que pour simuler un délai de traitement dans la fonction callback (a titre d’illustration).
    Ce n’est pas le code qui doit être présent à cet endroit.

Mais si je mets du code qui peut avoir un délai d’exécution (comme un appel serveur par exemple), mon soucis est que le démon attend la fin de la fonction callback pour ‹ résumer › ses opérations.

C’est peut être normal, je voulais juste avoir la confirmation de ma compréhension par @Mips.

Re,

J’ai dit quelque chose qu’il ne fallait pas ? je voulais simplement aider…

Il attend pour résumer ou bien il tente à nouveau (car il est dans sa boucle) d’envoyer un nouveau tick, mais la fonction await n’arrivant pas à envoyer des données au php car il est en pause (sleep), il attend/retry jusqu’à y arriver au bout de 10 sec ?
Pour le mettre en évidence, tu peux mettre un log juste avant et juste après l’appel au await self.send_to_jeedom({'fetch_events':f'tick'}), qui va apparaitre dans les logs.

Je te laisse voir avec notre ami @Mips

TiTidom.

Le send to jeedom est « bloquant » => oui il attend de recevoir un http 200 avant de passer à la ligne suivante

Si tu veux que le démon envoie des feedback sans attendre il faut plutôt utiliser le add_change

Donc si les sleep 10s sont dans le code php, c’est bien la cause

Merci de ton retour.

J’ai testé.
Par rapport au send_to_jeedom(), le add_change() n’est effectivement pas bloquant pour la routine Daemon mais, évidement, il attend aussi un code HTTP 200 et donc la réponse de la fonction de callback.
Toute latence dans le traitement callback retarde d’autant un nouvel envoi, ce qui est logique.
Et donc ca donne ca si je met un sleep(10) dans la fonction callback:

Je vais creuser mon architecture PHP pour m’adapter à cela.

Encore merci.

En même temps 10s de traitement ca me parait énorme

Non mais, c’était juste pour la démo :wink:

J’avais compris. Donc je ne suis pas certain que ca soit représentatif de la réalité (j’ai des contre-exemples aussi mais j’ai aussi une solution à proposer dans ce cas) :wink:

Je suis preneur de toute idée.

Je pensais implémenter un Pipe coté callback pour passer vers PHP mais c’est vari que c’est une solution un peu lourde.
I faut que je regarde si c’est vraiment nécessaire.

Il n’en reste pas moins que pour implémenter un démon de plugin Jeedom très facilement, ta lib est vraiment super :+1:
Bravo et merci :pray:

1 « J'aime »