[TUTO] Piloter un robot Roborock

Bonjour,

J’ai récemment fait l’acquisition d’un robot aspirateur-laveur Roborock, et même si l’appli est très bien faite j’ai bien entendu voulu l’interfacer avec Jeedom.
Il existe bien entendu le plugin-mirobot, mais je ne voulais pas me passer de l’appli Roborock en installant Mi Home, et de toute façon mon QRevo Master n’est pas compatible avec Mi Home.

Il se trouve que la bibliothèque python de l’intégration de Roborock dans Home Assistant est disponible séparément sur Github avec une API. De plus la documentation est assez complète (mais il manque des choses que j’ai dû trouver par moi-même). Elle supporte normalement tous les modèles Roborock y compris les plus récents (SAROS).

Merci à humbertogontijo et Lash-L !

Github : https://github.com/Python-roborock/python-roborock

Documentation : https://python-roborock.readthedocs.io/en/latest/index.html

N’ayant pas le niveau pour coder un plugin, je vous propose donc ma solution bricolée à base de bloc code et de scéharios. Bien entendu le mieux serait un vrai plugin, qui pourrait notamment exploiter la carte. Je ne suis pas de plus programmeur, donc mon code n’est probablement pas le plus propre :wink: Je suis donc ouvert à tout proposition de modification et/ou d’amélioration.

J’ai fait le choix d’installer la librairie sur un LXC Proxmox dédié, et de l’utiliser via l’excellent plugin-sshmanager.
Il est possible d’installer la librairie sur la même machine que Jeedom mais il faudra dans ce cas adapter mon bloc-code.

Je considère en pré-requis que :

  • python est installé sur le LXC
  • plugin-sshmanager est installé et l’équipment correspondant au LXC est mis en place dans le plugin.

Etape 1 : installation de la librairie

J’ai fait le choix de l’installer via pipx qui gère automatiquement le venv ppur l’application python.

apt install pipx

puis :

pipx install python-roborock

L’executable « roborock » créé va se trouver ensuite (si vous conservez les options par défaut) dans .local/pipx/venvs/python-roborock/bin/.

Etape 2 : Initialiser l’outil roborock

Se référer à la documentation : https://python-roborock.readthedocs.io/en/latest/usage.html

Sur le LXC

.local/pipx/venvs/python-roborock/bin/roborock login --email username --password password

L’application va ensuite gérer les requêtes en cache et maintenir la connexion. Elle va même gérer le nombre de requêtes maximum pour ne pas se faire bloquer son compte, même si je recommande d’être prudent car il semblerait que le SAV Roborock peut être parfois peu efficace dans le déblocage d’un compte.

Il faut ensuite déterminer l’ID de son Roborock :

.local/pipx/venvs/python-roborock/bin/roborock list-devices

Etape 3 : Créer un virtuel

Le status du robot sera récupéré dans un virtuel, qui sera mis à jour via le bloc-code.
A noter que la puissance électrique est présente dans mon virtuel, mais l’information ne provient pas de la librairie mais d’une prise zwave sur laquelle est branché le dock.


Etape 4 : Création du bloc-code dans un scénario

Créer un scénario avec le bloc-code suivant :

// Code pour piloter aspirateur Roborock sur Jeedom via la librairie python-roborock installée via pipx sur une machine distante
// Auteur : Madcow | Version 21/05/2025


///////////////////////////////////////

// On récupère les tags

$tags = $scenario->getTags();

$scenario->setLog('array des tags passés au scénario :' ."\n" .print_r($tags,true) ."\n");

///////////////////////////////////////

///////////////////////////////////////

//Variables à compléter par l'utilisateur

$sshManagerEqLogic = '#[Technique][Roborock_SSH]#';
$device_id = 'XXXXXXXXXX';

$water_mop_defaut = 200; // off
$puissance_aspiration_defaut = 102; // balanced
$mode_mop_defaut = 300; //standard
$repetition_defaut = 1;
  
//////////////////////////////////

// Récupération de l'ID de l'équipement SSH Manager

$sshManagerId = eqLogic::byString($sshManagerEqLogic)->getId();
//$scenario->setLog('ID :' ."\n" .$sshManagerId ."\n");


///////////////////////////////////////

$commande = $tags['#commande#'];

///////////////////////////////////////

// Pour commande Status

if($commande == 'status') {
	$commande_SSH = ".local/pipx/venvs/python-roborock/bin/roborock status --device_id $device_id";
  	
  	$output = sshmanager::executeCmds($sshManagerId, $commande_SSH); // dans le cas d'une commande Roborock permet de récupérer le get-status généré
	//$scenario->setLog('output SSHManager :' ."\n" .$output ."\n");
	
  	// Traitement de la sortie de la commande  pour récupérer le status 
	$output = strstr($output, '{');
	$output_array = explode('INFO', $output);
	//$scenario->setLog('explode :' ."\n" .print_r($output_array, true) ."\n");
	$json = $output_array[0];
	$json = str_replace("'", "\"", $json); // on remplace lea apostrophes par des guillemets. En effet à la différence de la commande directe get_status ici le retour de la commande roborock s'effectue avec des apostrophes ce qui empêche le json_decode de fonctionner
	//$scenario->setLog('json avant decode :' ."\n" .$json ."\n");
	$json_decode = json_decode($json,true);

	//$scenario->setLog('json décodé pour status roborock :' ."\n" .print_r($json_decode, true) ."\n");

	//Récuoération du status du robot dans un virtuel

	cmd::byString('#[Technique][Roborock][Status Robot]#')->event($json_decode[state]);
  	cmd::byString('#[Technique][Roborock][Status Robot Texte]#')->event($json_decode[stateName]);
   
  	cmd::byString('#[Technique][Roborock][Batterie]#')->event($json_decode[battery]);
  	
  	cmd::byString('#[Technique][Roborock][Code Erreur]#')->event($json_decode[errorCode]);
   	cmd::byString('#[Technique][Roborock][Code Erreur Texte]#')->event($json_decode[errorCodeName]);
  	cmd::byString('#[Technique][Roborock][Code Erreur Dock]#')->event($json_decode[dockErrorStatus]);
  
  
  	// Débit eau serpillière
  	cmd::byString('#[Technique][Roborock][Eau Mop]#')->event($json_decode[waterBoxMode]);
  	cmd::byString('#[Technique][Roborock][Eau Mop Texte]#')->event($json_decode[waterBoxModeName]);
    
  	// Mode serpillière
  	cmd::byString('#[Technique][Roborock][Mode Mop]#')->event($json_decode[mopMode]);
  	cmd::byString('#[Technique][Roborock][Mode Mop Texte]#')->event($json_decode[mopModeName]);
  	
  	// Puissance aspiration
  	cmd::byString('#[Technique][Roborock][Puissance Aspiration]#')->event($json_decode[fanPower]);
    cmd::byString('#[Technique][Roborock][Puissance Aspiration Texte]#')->event($json_decode[fanPowerName]);
    
  	//cmd::byString('#[Technique][Roborock][Sur le dock]#')->event($json_decode[chargeStatus]); // ne fonctionne pas
    cmd::byString('#[Technique][Roborock][Séchage Mop en cours]#')->event($json_decode[dryStatus]);
  	cmd::byString('#[Technique][Roborock][Temps Séchage Mop Restant]#')->event($json_decode[rdt]);
  	cmd::byString('#[Technique][Roborock][Mode Nettoyage]#')->event($json_decode[inCleaning]); // 0 : pas en cours de nettoyage
  	
  	// Informations sur dernier nettoyage effectué
  	cmd::byString('#[Technique][Roborock][Superficie Dernier Nettoyage]#')->event($json_decode[squareMeterCleanArea]);
    cmd::byString('#[Technique][Roborock][Durée Dernier Nettoyage]#')->event($json_decode[cleanTime]);
    
  	cmd::byString('#[Technique][Roborock][Pourcentage Nettoyage Restant]#')->event($json_decode[cleanPercent]);

  
	//Transcription du code erreur dock en texte car il n'y a pas de champ texte retourné par la commande Status
  	$erreur_dock_traduction = array(
		0 => "ok",
		34 => "duct_blockage",
		38 => "water_empty",
		39 => "waste_water_tank_full",
		42 => "maintenance_brush_jammed",
		44 => "dirty_tank_latch_open",
		46 => "no_dustbin",
		53 => "cleaning_tank_full_or_blocked",
	);

	foreach($erreur_dock_traduction as $key => $value) {
    	if ($key == $json_decode[dockErrorStatus]) {
        	cmd::byString('#[Technique][Roborock][Code Erreur Dock Texte]#')->event($value);
        }
	}
      
 }

///////////////////////////////////////

//Pour créer un array avec la correspondance id <-> nom des pièces

if($commande == 'room_list') {
	$commande_SSH = ".local/pipx/venvs/python-roborock/bin/roborock -d command --device_id $device_id --cmd get_multi_maps_list";
  	$output = sshmanager::executeCmds($sshManagerId, $commande_SSH); // dans le cas d'une commande Roborock permet de récupérer le get-status généré
	//$scenario->setLog('output SSHManager :' ."\n" .$output ."\n");
	// traitement de la sortie de la commande  pour récupérer le status pour les commandes qui le demande
	$output = strstr($output, '{');
	$output_array = explode('INFO', $output);
	//$scenario->setLog('explode :' ."\n" .print_r($output_array, true) ."\n");
	$json = $output_array[0];
	$json = str_replace("'", "\"", $json); // on remplace lea apostrophes par des guillemets. En effet à la différence de la commande directe get_status ici le retour de la commande roborock s'effectue avec des apostrophes ce qui empêche le json_decode
	//$scenario->setLog('json avant decode :' ."\n" .$json ."\n");
	$json_decode = json_decode($json,true);


	//$scenario->setLog('json décodé pour liste des pièces :' ."\n" .print_r($json_decode, true) ."\n");


// Création d'un array avec id et noms des pièces correspondantes

	foreach ($json_decode[map_info][0][rooms] as $key => $value) {
		
		$room_correspondance[$value[id]] = $value[iot_name];
		//$scenario->setLog('id :' .$value[id] .' / nom : ' .$value[iot_name]."\n");
    }
	
  	$scenario->setLog('Correspondance pièces :' ."\n" .print_r($room_correspondance,true) ."\n");  
 
}

///////////////////////////////////////

///////////////////////////////////////

//Pour créer un array avec la correspondance id <-> nom des routines

if($commande == 'scene_list') {
	$commande_SSH = ".local/pipx/venvs/python-roborock/bin/roborock list-scenes --device_id $device_id";
  	$output = sshmanager::executeCmds($sshManagerId, $commande_SSH); // dans le cas d'une commande Roborock permet de récupérer le get-status généré
	//$scenario->setLog('output SSHManager :' ."\n" .$output ."\n");
	// traitement de la sortie de la commande  pour récupérer le status pour les commandes qui le demande
	$output = substr($output, 5, strlen($output)-8);
	$output_array = explode(',', $output);
	
  	//$scenario->setLog('explode :' ."\n" .print_r($output_array, true) ."\n");

// Création d'un array avec id et noms des routines correspondantes

	foreach($output_array as $key => $value) {
		if (strpos($value,"id") != false) {
        	$id = substr($value,strpos($value, ": ")+2);
        	$scene_correspondance[$id] = substr($output_array[$key+1],strpos($output_array[$key+1], ": ")+2);
        	$scene_correspondance[$id] = str_replace("}","", $scene_correspondance[$id]);
          	$scene_correspondance[$id] = str_replace("\"","", $scene_correspondance[$id]);
        	$scene_correspondance[$id] = trim($scene_correspondance[$id]);
		}
    }
  	
	$scenario->setLog('Correspondance routines :' ."\n" .print_r($scene_correspondance,true) ."\n");

}

///////////////////////////////////////

//Pour lancer un nettoyage par pièce

if($commande == 'segment') {

  	// On règle la quantité d'eau de la mop (=200 : pas de mop)
  
  	if ($tags['#water_mop#'] !='') {
  		$water_mop = $tags['#water_mop#'];
    } else {
    	$water_mop = $water_mop_defaut;
    }
  	
  	$commande_SSH = '.local/pipx/venvs/python-roborock/bin/roborock -d command --device_id ' .$device_id .' --cmd set_water_box_custom_mode --params ' ."'[" .$water_mop ."]'";
  	
  	$output = sshmanager::executeCmds($sshManagerId, $commande_SSH); // dans le cas d'une commande Roborock permet de récupérer le get-status généré
	//$scenario->setLog('output SSHManager :' ."\n" .$output ."\n");
	
  	// traitement de la sortie de la commande  pour récupérer le status pour les commandes qui le demande
	$output = strstr($output, '{');
	$output_array = explode('INFO', $output);
	//$scenario->setLog('explode :' ."\n" .print_r($output_array, true) ."\n");
	$json = $output_array[0];
	$json = str_replace("'", "\"", $json); // on remplace les apostrophes par des guillemets. En effet à la différence de la commande directe get_status ici le retour de la commande roborock s'effectue avec des apostrophes ce qui empêche le json_decode
	//$scenario->setLog('json avant decode :' ."\n" .$json ."\n");
	$json_decode = json_decode($json,true);

	//$scenario->setLog('json décodé pour status roborock :' ."\n" .print_r($json_decode, true) ."\n");
	$scenario->setLog('retour water box custom mode :' ."\n" .$json_decode[water_box_mode] ."\n");


	// On règle le mode de mop
  	
  	if ($water_mop != 200) {  // aucun intérêt à régler le mode de la mop si celle-ci est désactivée
  
  		if ($tags['#mode_mop#'] != '') {
  			$mode_mop = $tags['#mode_mop#'];
   		} else {
    		$mode_mop = $mode_mop_defaut;
    	}  
    
  		$commande_SSH = '.local/pipx/venvs/python-roborock/bin/roborock -d command --device_id ' .$device_id .' --cmd set_mop_mode --params ' ."'[" .$mode_mop ."]'";
  	
  		$output = sshmanager::executeCmds($sshManagerId, $commande_SSH); // dans le cas d'une commande Roborock permet de récupérer le get-status généré
		//$scenario->setLog('ouput SSHManager :' ."\n" .$output ."\n");
  	
 		// traitement de la sortie de la commande  pour récupérer le status pour les commandes qui le demande
		$output = strstr($output, '{');
		$output_array = explode('INFO', $output);
		//$scenario->setLog('explode :' ."\n" .print_r($output_array, true) ."\n");
		$json = $output_array[0];
		$json = str_replace("'", "\"", $json); // on remplace lew apostrophes par des guillemets. En effet à la différence de la commande directe get_status ici le retour de la commande roborock s'effectue avec des apostrophes ce qui empêche le json_decode
		//$scenario->setLog('json avant decode :' ."\n" .$json ."\n");
		$json_decode = json_decode($json,true);


		//$scenario->setLog('json décodé pour status roborock :' ."\n" .print_r($json_decode, true) ."\n");
		$scenario->setLog('retour mop mode :' ."\n" .$json_decode[mop_mode] ."\n");
    }
  	

	// On règle la puissance d'aspiration

  	if ($tags['#aspiration#'] != '') {
  		$puissance_aspiration = $tags['#aspiration#'];
    } else {
    	$puissance_aspiration = $puissance_aspiration_defaut;
    }  
    
  	$commande_SSH = '.local/pipx/venvs/python-roborock/bin/roborock -d command --device_id ' .$device_id .' --cmd set_custom_mode --params ' ."'[" .$puissance_aspiration ."]'";
  	
  	$output = sshmanager::executeCmds($sshManagerId, $commande_SSH); // dans le cas d'une commande Roborock permet de récupérer le get-status généré
	//$scenario->setLog('output SSHManager :' ."\n" .$output ."\n");
  	
  	$position = strpos($output,'get_custom_mode: ');
  	$retour_commande = substr($output, $position+17,3);   	//offset de 17 qui correspond à la longueur de 'get_custom_mode: '
	
	$scenario->setLog('retour custom mode :' ."\n" .$retour_commande ."\n");

  
  	// On lance le nettoyage par segment avec l'id de la pièce
  
    if ($tags['#repetition#'] != '') {
        $repetition = $tags['#repetition#'];
    } else {
    	$repetition = $repetition_defaut;
    }
   	
  	// Lacncement nettoyage dans la pièce souhaitée
  	if ($tags['#piece#'] != '') {
  		$piece = $tags['#piece#'];
    	$commande_SSH = '.local/pipx/venvs/python-roborock/bin/roborock -d command --device_id ' .$device_id .' --cmd app_segment_clean --params ' ."'[{" .'"segments": ' ."[" .$piece ."], " .'"repeat": ' .$repetition ."}]'";
		sshmanager::executeCmds($sshManagerId, $commande_SSH); // Pas de sortie pertinente suite à cette commande
      
      	if ($water_mop != 200) {  // log différent entre aspiration seule et aspiration + séchage (mode mop seule non-considéré)
  			$scenario->setLog('Lancement nettoyage aspiration et serpillière dans pièces ' .$piece .' avec aspiration ' .$puissance_aspiration .', mode serpillage ' .$mode_mop .' et quantité d\'eau ' .$water_mop .', avec répétition ' .$repetition ."\n");
    	} else {
			$scenario->setLog('Lancement nettoyage aspiration dans pièces ' .$piece .' avec aspiration ' .$puissance_aspiration .', et répétition ' .$repetition ."\n");
    	}
      
    } else {
    	$scenario->setLog('___ERREUR COMMANDE SEGMENT : pièce non-définie' ."\n");
    }  
  	

}


///////////////////////////////////////

// Pour lancer une routine

if($commande == 'scene') {

    if ($tags['#scene_id#'] != '') {
  		$scene_id = $tags['#scene_id#'];
  		
      	$commande_SSH = ".local/pipx/venvs/python-roborock/bin/roborock execute-scene --scene_id $scene_id";
  		sshmanager::executeCmds($sshManagerId, $commande_SSH); // Pas de sortie suite à cette commande
        	
  		$scenario->setLog('Routine N° ' .$scene_id .' lancée' ."\n");
    
    } else {
    	$scenario->setLog('___ERREUR COMMANDE SCENE :  Routine non-définie' ."\n");
    }  	
  
}

///////////////////////////////////////

Ensuite, ce scénario, qui sert uniquement d’interface avec l’application python, sera appelé par un autre scénario via tags.

Il faut compléter la partie « Variables » au début du code avec vos propres données.

Le nom de mon virtuel est en dur dans le bloc code dans la partie « status ». Il vous faudra l’adapter selon le nom de votre propre virtuel.

Le tag principal qui correspond à la commande souhaitée est commande.

Valeurs possibles :

  • status : mise à jour du virtuel avec des informations sur le robot

    • pas d’autres tags à passer en même temps
  • room_list : retourne dans le log un tableau avec les correspondances ID <> nom des pièces

    • pas d’autres tags à passer en même temps
  • scene_list : retourne dans le log un tableau avec les correspondances ID <> nom des routines

    • pas d’autres tags à passer en même temps
  • scene : pour lancer une routine. Autre tag à passser en même temps :

    • scene_id : ID de la routine
  • segment : nettoyage d’une ou plusieur pièces. Autres tags à passer en même temps (certains facultatifs car ils comportent des valeurs par défaut, valeurs réglables dans le bloc-code si besoin) :

    • piece : ID de la pièce. Si plusieurs pièces : ID séparées par des virgules (exemple : 19,25)

    • aspiration : puissance d’aspiration

      • défaut : 102 (balanced)
      • pour off : 105 (donc mop seulement)
    • water_mop : quantité d’eau pour la mop

      • défaut : 200 (off donc aspiration seulement)
    • mode_mop : mode de passage de la mop

      • défaut : 300 (standard)
    • repetition : répétition du nettoyage par pièce

      • défaut : 1

Quelques précisions :

  • les codes pour l’aspiration et la mop sont ceux de mon QRevo Master. Il faudra peut-être les adapter selon le robot dont vous disposez. Voir également ce fichier qui pourrait vous aider : https://github.com/Python-roborock/python-roborock/blob/main/roborock/code_mappings.py
  • il faut lancer la commande « room_list » au moins une fois pour avoir la correspondance ID <> nom des pièces. L’information sera alors dans le log du scénario.
  • votre robot ne supporte peut-être pas les routines (appelées ici scene)
  • il faut lancer la commande « scene_list » au moins une fois pour avoir la correspondance ID <> nom des routines. L’information sera alors dans le log du scénario.

Etape 5 : Exemple de scénario de contrôle

Dans mon cas, j’utilise le plugin-import2calendar pour programmer mon robot via un agenda Google.


J’utilise ce scénario qui permet :

  • de rafraîchir le status du robot toutes les 15 minutes quand je n’ai pas lancé via Jeedom un nettoyage
  • de lancer des routines selon un tag passé par plugin-import2calendar.

Pourquoi ne pas lancer directement à partir de plugin-import2calendar ? Parce que ce scénario me permet d’augmenter la fréquence de rafraîchissement du status du robot toutes les 5 minutes quand une routine est lancée tant qu’elle est en cours au lieu de toutes les 15 min.

N’hésitez pas à réagir si vous avez des questions ou des commentaires.
La librairie permet également de lancer un nettoyage par zone, de déplacer le robot à des coordonnées, et même de récupérer la carte complète. Je n’ai pas tout implémenté soit parce que je n’en avais pas l’utilité , soit parce
qu’il faudrait un plugin pour cela (comme la carte).

10 « J'aime »

Bravo et merci pour le partage

J’ai aussi un roborock, je vais tester ça dès que possible :wink:

Je poste pour suivre. Je me demande avec les avancés de l’IA si elle ne peut pas créer un plugin à partir de librairie ? J’ai essayé mais ca n’a pas été concluant avec un accès gratuit qui te bloque rapidement. En gros j’ai crée la structure du plugin, les dépendances s’installent bien mais le démon ne démarre pas suite à un problème pour faire l’authentification. Sur HA quand tu te connectes avec ton mail immédiatement cela envois un code sur ton mail que tu rentres dans HA et ensuite il récupère tes équipements. Sur Jeedom si j’utilise la connexion roborock login --email username --password password je recois une erreur de connexion qui me dit que

Avez vous eu le meme probleme ?

Bonjour,

Il manque le message d’erreur reçu dans ton message :wink:
Moi je n’ai aucune double-authentification sur mon compte Roborock très récent. J’ai juste une notification de connexion sur l’application.

Bonjour,

La bibliothèque a été mise à jour et permet la connexion avec un code :wink: