Erreur au lancement de mon démon

Hello à tous,

Je suis en train de développer mon tout premier démon et je vais avoir besoin d’un coup de main.
Tout d’abord un grand merci à @nebz et @Mips pour la mise à disposition de la gestion des dépendances et la lib « jeedomdaemon » :pray: :pray: :pray:
Ca m’a grandement simplifié les choses.

L’objectif de ce démon est de récupérer des messages MQTT envoyés depuis un broker externe !
J’ai donc installer un pyenv avec la librairie paho-mqqt qui me servira de client MQTT.
Jusque là tout va bien.

Je viens également de finir le code de mon démon :

import ssl
import json
import asyncio
import paho.mqtt.client as mqtt

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


class DaemonConfig(BaseConfig):
    
    def __init__(self):
        super().__init__()

        self.add_argument("--host", help="MQTT host", type=str)
        self.add_argument("--port", help="MQTT port", type=int)
        self.add_argument("--username", help="MQTT username", type=str)
        self.add_argument("--password", help="MQTT password (ID token)", type=str)
        
    @property
    def mqtt_host(self): return str(self._args.host)
    @property
    def mqtt_port(self): return int(self._args.port)
    @property
    def mqtt_username(self): return str(self._args.username)
    @property
    def mqtt_password(self): return str(self._args.password)
    

class MyBMWDaemon(BaseDaemon):
    def __init__(self) -> None:
        self._config = DaemonConfig()
        super().__init__(self._config, self.on_start, self.on_message, self.on_stop)
        self._mqtt_client = None
        self._subscriptions = set()
        self._connected = False
        self._loop = None

       
    async def on_start(self):
        """ Daemon initialization and MQTT connection """
        self._logger.info("Starting myBMW daemon...")
        self._loop = asyncio.get_running_loop()
        await self.__connect_mqtt()


    async def on_message(self, message: dict):
        """ Message received from Jeedom """
        try:
            action = message['action']
            vin = message['vin']
            topic = f"{self._config.mqtt_username}/{vin}"
                       
            if action == 'subscribe':
                if topic not in self._subscriptions:
                    self._subscriptions.add(topic)
                    if self._connected:
                        self._mqtt_client.subscribe(topic)
                        self._logger.info(f"Subscribed to topic {topic}")
                    else:
                        self._logger.warning(f"Will subscribe to topic {topic} after reconnect")

            elif  action == 'unsubscribe':
                if topic in self._subscriptions:
                    self._subscriptions.remove(topic)
                    if self._connected:
                        self._mqtt_client.unsubscribe(topic)
                    self._logger.info(f"Unsubscribed from topic {topic}")
        
        except Exception as e:
            self._logger.error(f"Error handling message from Jeedom: {e}")


    async def on_stop(self):
        """ MQTT disconnection """
        self._logger.info("Stopping myBMW daemon...")
        if self._mqtt_client:
            self._mqtt_client.loop_stop()
            self._mqtt_client.disconnect()
        self._subscriptions.clear()


    async def __connect_mqtt(self):
        """ Secure connection to BMW MQTT broker """
        self._logger.info("Connecting to BMW MQTT Broker...")

        def on_connect(client, userdata, flags, rc):
            if rc == 0:
                self._connected = True
                self._logger.info(f"Connected successfully to BMW MQTT broker {self._config.mqtt_host}:{self._config.mqtt_port}")
                for topic in self._subscriptions:
                    client.subscribe(topic)
                    self._logger.info(f"Re-subscribed to {topic}")
            else:
                self._logger.error(f"MQTT connection failed with code {rc}")

        def on_message(client, userdata, msg):
            try:
                payload = json.loads(msg.payload.decode("utf-8"))
                asyncio.run_coroutine_threadsafe(
                    self.send_to_jeedom({"topic": msg.topic, "data": payload}),
                    self._loop
                )
                self._logger.debug(f"Message received on {msg.topic} : {payload}")
            except Exception as e:
                self._logger.error(f"Error processing MQTT message: {e}")

        def on_disconnect(client, userdata, rc):
            self._logger.warning(f"Disconnected from MQTT broker (code {rc})")
            self._connected = False


        # MQTT client initialization
        self._mqtt_client = mqtt.Client()
        self._mqtt_client.username_pw_set(self._config.mqtt_username, self._config.mqtt_password)
        self._mqtt_client.on_connect = on_connect
        self._mqtt_client.on_message = on_message
        self._mqtt_client.on_disconnect = on_disconnect
        self._mqtt_client.reconnect_delay_set(min_delay=5, max_delay=60)

        # BMW TLS configuration
        self._mqtt_client.tls_set(cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2)
        self._mqtt_client.tls_insecure_set(False)

        try:
            self._mqtt_client.connect(self._config.mqtt_host, self._config.mqtt_port, keepalive=60)
            self._mqtt_client.loop_start()
        except Exception as e:
            self._logger.error(f"Failed to connect to MQTT broker: {e}")


MyBMWDaemon().run()

Ainsi que la fonction php qui le lance :

    public static function deamon_start()
	{
		self::deamon_stop();
       	$deamon_info = self::deamon_info();
        if ($deamon_info['launchable'] != 'ok') {
            throw new Exception('Please check the configuration');
        }

		$username = config::byKey('username', __CLASS__);
		$password = self::getIdToken();
		$host = config::byKey('host', __CLASS__) ?: 'customer.streaming-cardata.bmwgroup.com';
		$port = (int) (config::byKey('port', __CLASS__) ?: 9000);
		
		$path = realpath(__DIR__ . '/../../resources');
        $cmd = self::PYTHON_PATH . " {$path}/myBMW.py";
        $cmd .= ' --loglevel ' . log::convertLogLevel(log::getLogLevel(__CLASS__));
        $cmd .= ' --host ' . $host;
        $cmd .= ' --port ' . $port;
        $cmd .= ' --username ' . escapeshellarg(trim($username));
        $cmd .= ' --password ' . escapeshellarg(trim($password));
		$cmd .= ' --callback ' . network::getNetworkAccess('internal', 'proto:127.0.0.1:port:comp') . '/plugins/myBMW/core/php/jeemyBMW.php';
        $cmd .= ' --apikey ' . jeedom::getApiKey(__CLASS__);
        $cmd .= ' --pid ' . jeedom::getTmpFolder(__CLASS__) . '/daemon.pid';
        log::add(__CLASS__, 'debug', 'Lancement démon');
        log::add('myBMW', 'debug', 'CMD= ' . $cmd);
		$result = exec($cmd . ' >> ' . log::getPathToLog(__CLASS__ . '_daemon') . ' 2>&1 &');

		$i = 0;
        while ($i < 10) {
            $deamon_info = self::deamon_info();
            if ($deamon_info['state'] == 'ok') {
                break;
            }
            sleep(1);
            $i++;
        }
        if ($i >= 10) {
            log::add(__CLASS__, 'error', 'Unable to start daemon', 'unableStartDeamon');
            return false;
        }
        message::removeAll(__CLASS__, 'unableStartDeamon');

        return true;
	}

Mais quand je lance le démon, j’ai l’erreur suivante :

0024|[2025-10-28 19:52:52] INFO  : Starting daemon (lib version 1.2.9) with log level: debug
0025|[2025-10-28 19:52:52] DEBUG  : Writing PID 8626 to /tmp/jeedom/myBMW/daemon.pid
0026|[2025-10-28 19:52:52] ERROR  : Fatal error: (<class 'ValueError'>) in /var/www/html/plugins/myBMW/resources/venv/lib/python3.11/site-packages/jeedomdaemon/base_daemon.py on line 90
0027|[2025-10-28 19:52:52] INFO  : Shutdown
0028|[2025-10-28 19:52:52] DEBUG  : Removing PID file /tmp/jeedom/myBMW/daemon.pid
0029|[2025-10-28 19:52:52] DEBUG  : Exit 0

J’ai beau vérifier les arguments (si cela vient de là), je ne vois pas d’erreur :frowning:
Côté base_daemon.py : l’erreur se produit sur la ligne

 asyncio.run(self.__run())

Avis aux experts python pour me donner une piste :slight_smile:
Merci d’avance

Xav

Salut,

Jamais utilisé cette lib qui m’aurait sûrement épargné bien des soucis et dont j’ai appris l’existence que récemment, mais, sauf erreur de ma part, elle semble attendre un config.socket_port entre 1024 et 65535 non inclus :

1 « J'aime »

:pray: :pray: un grand merci @Aurelien j’étais complètement passé à côté de ca !

Bonjour,

De mémoire, le plugin worxlandroidS utilise jeedomdaemon et un client mqtt. Ça peut faire un exemple très proche de ce que vous souhaitez et le dépôt GitHub de Mips est public.

À+
Michel

Hello @Michel_F et merci

Je me suis appuyé dessus en effet :wink: Idem avec le plugin kroomba !
Que ferait-on sans Mips :stuck_out_tongue:

Tout est fonctionnel maintenant ! Reste plus qu’à traiter les messages reçus :muscle:

3 « J'aime »

Le message d’erreur dans le log n’est pas clair du tout, je vais fixer ca

2 « J'aime »

au fait, depuis une version relativement récente, tu peux écrire cela de façon plus esthétique:

à modifier en:

from jeedomdaemon import BaseDaemon, BaseConfig

donc plus besoin du .base_daemon ou .base_config

Correction faite :wink:

Voici ma version finale avec gestion du refresh token, si tu as 2min pour jeter un oeil et me dire ce que tu en penses :pray:

import ssl
import json
import asyncio
import paho.mqtt.client as mqtt

from jeedomdaemon import BaseDaemon, BaseConfig


class DaemonConfig(BaseConfig):
    
    def __init__(self):
        super().__init__()

        self.add_argument("--host", help="MQTT host", type=str)
        self.add_argument("--port", help="MQTT port", type=int)
        self.add_argument("--username", help="MQTT username", type=str)
        self.add_argument("--password", help="MQTT password (ID token)", type=str)
        
    @property
    def mqtt_host(self): return str(self._args.host)
    @property
    def mqtt_port(self): return int(self._args.port)
    @property
    def mqtt_username(self): return str(self._args.username)
    @property
    def mqtt_password(self): return str(self._args.password)
    

class MyBMWDaemon(BaseDaemon):
    def __init__(self) -> None:
        self._config = DaemonConfig()
        super().__init__(self._config, self.on_start, self.on_message, self.on_stop)
        self._mqtt_client = None
        self._subscriptions = set()
        self._connected = False
        self._loop = None

       
    async def on_start(self):
        self._logger.info("Starting myBMW daemon...")
        self._loop = asyncio.get_running_loop()
        self._init_mqtt()
        self._logger.info("Connecting to BMW MQTT Broker...")
        try:
            self._mqtt_client.connect(self._config.mqtt_host, self._config.mqtt_port, keepalive=45)
            self._mqtt_client.loop_start()
        except Exception as e:
            self._logger.error(f"Failed to connect to MQTT broker: {e}")


    async def on_message(self, message: dict):
        try:
            action = message['action']
            param = message['param']
                                   
            if action == 'refreshToken':
                if param:
                    self._logger.info("Received new token from Jeedom")
                    self._config._args.password = param
                    self._mqtt_client.username_pw_set(self._config.mqtt_username, param)
                    await asyncio.sleep(2)
                    try:
                        self._logger.info("Reconnecting to MQTT broker with new token...")
                        self._mqtt_client.reconnect()
                    except Exception as e:
                        self._logger.error(f"Failed to connect to MQTT broker after token refresh: {e}")
                else:
                    self._logger.warning("Received no token data")
                return            
            
            if action == 'subscribe':
                topic = f"{self._config.mqtt_username}/{param}"
                if topic not in self._subscriptions:
                    self._subscriptions.add(topic)
                    if self._connected:
                        self._mqtt_client.subscribe(topic)
                        self._logger.info(f"Subscribed to topic {topic}")
                    else:
                        self._logger.warning(f"Will subscribe to topic {topic} after reconnect")
                    return

            elif  action == 'unsubscribe':
                topic = f"{self._config.mqtt_username}/{param}"
                if topic in self._subscriptions:
                    self._subscriptions.remove(topic)
                    if self._connected:
                        self._mqtt_client.unsubscribe(topic)
                    self._logger.info(f"Unsubscribed from topic {topic}")
        
        except Exception as e:
            self._logger.error(f"Error handling message from Jeedom: {e}")


    async def on_stop(self):
        self._logger.info("Stopping myBMW daemon...")
        if self._mqtt_client:
            self._mqtt_client.loop_stop()
            self._mqtt_client.disconnect()
        self._subscriptions.clear()
        self._logger.info("Daemon stopped")


    def _init_mqtt(self):
        # MQTT client initialization
        self._mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
        self._mqtt_client.username_pw_set(self._config.mqtt_username, self._config.mqtt_password)
        self._mqtt_client.on_connect = self._on_connect
        self._mqtt_client.on_message = self._on_message
        self._mqtt_client.on_disconnect = self._on_disconnect
        self._mqtt_client.reconnect_delay_set(min_delay=10, max_delay=300)
        # BMW TLS configuration
        tls_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
        tls_context.check_hostname = True
        tls_context.verify_mode = ssl.CERT_REQUIRED
        tls_context.minimum_version = ssl.TLSVersion.TLSv1_2
        self._mqtt_client.tls_set_context(tls_context)
        self._mqtt_client.tls_insecure_set(False)
        
        
    def _on_connect(self, client, userdata, flags, reasonCode, properties=None, *args):
        if reasonCode == mqtt.CONNACK_ACCEPTED:
            self._connected = True
            self._logger.info(f"Connected successfully to BMW MQTT broker {self._config.mqtt_host}:{self._config.mqtt_port}")
            for topic in self._subscriptions:
                client.subscribe(topic)
                self._logger.info(f"Re-subscribed to {topic}")
        else:
            self._connected = False
            self._logger.error(f"MQTT connection failed with code {reasonCode}")

            if "bad user name or password" in str(reasonCode).lower():
                self._logger.warning("Authentication failed — Refresh token required")
                asyncio.run_coroutine_threadsafe(
                    self.send_to_jeedom({"event": "refresh_token_required", "reason": str(reasonCode)}),
                    self._loop
                )

    def _on_disconnect(self, client, userdata, reasonCode, properties=None, *args):
        self._connected = False
        self._logger.warning(f"Disconnected from MQTT broker (code {reasonCode})")
        

    def _on_message(self, client, userdata, msg):
        try:
            payload = json.loads(msg.payload.decode("utf-8"))
            asyncio.run_coroutine_threadsafe(
                self.send_to_jeedom({"topic": msg.topic, "data": payload}),
                self._loop
            )
            self._logger.debug(f"Message received on {msg.topic} : {payload}")
        except Exception as e:
            self._logger.error(f"Error processing MQTT message: {e}")


MyBMWDaemon().run()
self._loop = None

ou

self._loop = asyncio.get_running_loop()

=> à ne pas faire, la class de base créé déjà cette propriété, tu peux l’utiliser directement, pas besoin de la redéclarer ni surtout d’écraser la valeur

pour le reste je n’ai rien remarque de particulier

1 « J'aime »

un grand merci à toi !
Lignes supprimées

Ce sujet a été automatiquement fermé après 24 heures suivant le dernier commentaire. Aucune réponse n’est permise dorénavant.