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 

Il ne faut pas indiquer de mot de passe pour recevoir son code de connexion sur l’adresse mail liée au compte Roborock (merci @rootard).

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 compatible PHP8 pour piloter aspirateur Roborock sur Jeedom via la librairie python-roborock 4.2.0 installée via pipx sur une machine distante
// Auteur : Madcow | Version 08/01/2026

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

// 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 = 'XXXXXXXXXXXXX';

$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 -d command --device_id $device_id --cmd get_status";
  	
  	$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, 'result":[');
  	$output = strstr($output, '{');
  	$json = strstr($output, '}', true) .'}';
	//$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écupération du status du robot dans un virtuel
	
if(isset($json_decode)) {

	cmd::byString('#[Technique][Roborock Alita][Status Robot]#')->event($json_decode['state']);
   
  	cmd::byString('#[Technique][Roborock Alita][Batterie]#')->event($json_decode['battery']);
  	
  	cmd::byString('#[Technique][Roborock Alita][Code Erreur]#')->event($json_decode['error_code']);

  	cmd::byString('#[Technique][Roborock Alita][Code Erreur Dock]#')->event($json_decode['dock_error_status']);
    
  	// Débit eau serpillière
  	cmd::byString('#[Technique][Roborock Alita][Eau Mop]#')->event($json_decode['water_box_mode']);
    
  	// Mode serpillière
  	cmd::byString('#[Technique][Roborock Alita][Mode Mop]#')->event($json_decode['mop_mode']);
  	
  	// Puissance aspiration
  	cmd::byString('#[Technique][Roborock Alita][Puissance Aspiration]#')->event($json_decode['fan_power']);
    
    cmd::byString('#[Technique][Roborock Alita][Séchage Mop en cours]#')->event($json_decode['dry_status']);
    
  	cmd::byString('#[Technique][Roborock Alita][Temps Séchage Mop Restant]#')->event($json_decode['rdt']);
  	
  	cmd::byString('#[Technique][Roborock Alita][Mode Nettoyage]#')->event($json_decode['in_cleaning']); // 0 : pas en cours de nettoyage
  	
  	// Informations sur dernier nettoyage effectué
  	cmd::byString('#[Technique][Roborock Alita][Superficie Dernier Nettoyage]#')->event($json_decode['clean_area']); // A diviser par 1000000 pour avoir des m2
    cmd::byString('#[Technique][Roborock Alita][Durée Dernier Nettoyage]#')->event($json_decode['clean_time']); // A diviser par 60 pour avoir des minutes
    
  	cmd::byString('#[Technique][Roborock Alita][Pourcentage Nettoyage Réalisé]#')->event($json_decode['clean_percent']);


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

// Transcription des différents codes
  
	//Transcription du code dock_error_status en texte car il n'y a pas de champ texte retourné par la commande Status
  	$dock_error_status_traduction = array(
		0 => "none",
		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($dock_error_status_traduction as $key => $value) {
    	if ($key == $json_decode['dock_error_status']) {
        	cmd::byString('#[Technique][Roborock Alita][Code Erreur Dock Texte]#')->event($value);
        }
	}
	
  	//Transcription du code fan_power en texte car il n'y a pas de champ texte retourné par la commande Status
  	$fan_power_traduction = array(
 		105 => "off", 
 		101 => "quiet",
 		102 => "balanced", 
 		103 => "turbo", 
 		104 => "max", 
 		106 => "custom", 
 		108 => "max_plus", 
 		110 => "smart_mode",
	);

	foreach($fan_power_traduction as $key => $value) {
    	if ($key == $json_decode['fan_power']) {
        	cmd::byString('#[Technique][Roborock Alita][Puissance Aspiration Texte]#')->event($value);
        }
	}

  
  	//Transcription du code state en texte car il n'y a pas de champ texte retourné par la commande Status
  	$state_traduction = array(
		0 => "unknown",
		1 => "starting",
		2 => "charger_disconnected",
		3 => "idle",
		4 => "remote_control_active",
		5 => "cleaning",
		6 => "returning_home",
		7 => "manual_mode",
		8 => "charging",
		9 => "charging_problem",
		10 => "paused",
		11 => "spot_cleaning",
		12 => "error",
		13 => "shutting_down",
		14 => "updating",
		15 => "docking",
		16 => "going_to_target",
		17 => "zoned_cleaning",
		18 => "segment_cleaning",
		22 => "emptying_the_bin",
		23 => "washing_the_mop",
		25 => "washing_the_mop_2",
		26 => "going_to_wash_the_mop",
		28 => "in_call",
		29 => "mapping",
		30 => "egg_attack",
		32 => "patrol",
		33 => "attaching_the_mop",
		34 => "detaching_the_mop",
		100 => "charging_complete",
		101 => "device_offline",
		103 => "locked",
		202 => "air_drying_stopping",
		6301 => "robot_status_mopping",
		6302 => "clean_mop_cleaning",
		6303 => "clean_mop_mopping",
		6304 => "segment_mopping",
		6305 => "segment_clean_mop_cleaning",
		6306 => "segment_clean_mop_mopping",
		6307 => "zoned_mopping",
		6308 => "zoned_clean_mop_cleaning",
		6309 => "zoned_clean_mop_mopping",
		6310 => "back_to_dock_washing_duster",
	);
 
	foreach($state_traduction as $key => $value) {
    	if ($key == $json_decode['state']) {
        	if (($json_decode['state'] == 8) && ($json_decode['battery'] == 100)) {
				cmd::byString('#[Technique][Roborock Alita][Status Robot Texte]#')->event('charged');
            } else {
              cmd::byString('#[Technique][Roborock Alita][Status Robot Texte]#')->event($value);
            }
        }
	}

  
    //Transcription du code water_box_mode en texte car il n'y a pas de champ texte retourné par la commande Status
  	$water_box_mode_traduction = array(
		200 => "off ",
 		201 => "low ",
 		202 => "medium ",
 		203 => "high ",
 		204 => "custom ",
 		207 => "custom_water_flow ",
 		209 => "smart_mode ",
	);

	foreach($water_box_mode_traduction as $key => $value) {
    	if ($key == $json_decode['water_box_mode']) {
        	cmd::byString('#[Technique][Roborock Alita][Eau Mop Texte]#')->event($value);
        }
	}

    //Transcription du code mop_mode en texte car il n'y a pas de champ texte retourné par la commande Status
  	$mop_mode_traduction = array(
		300 => "standard ",
		301 => "deep ",
		302 => "custom ",
		303 => "deep_plus ",
		304 => "fast ",
		306 => "smart_mode ",
	);

	foreach($mop_mode_traduction as $key => $value) {
    	if ($key == $json_decode['mop_mode']) {
        	cmd::byString('#[Technique][Roborock Alita][Mode Mop Texte]#')->event($value);
        }
	}

    //Transcription du code error_code en texte car il n'y a pas de champ texte retourné par la commande Status
  	$error_code_traduction = array(
		0 => "none",
		1 => "lidar_blocked",
		2 => "bumper_stuck",
		3 => "wheels_suspended",
		4 => "cliff_sensor_error",
		5 => "main_brush_jammed",
		6 => "side_brush_jammed",
		7 => "wheels_jammed",
		8 => "robot_trapped",
		9 => "no_dustbin",
		10 => "Filter is wet or blocked",
		11 => "Strong magnetic field detected",
		12 => "low_battery",
		13 => "charging_error",
		14 => "battery_error",
		15 => "wall_sensor_dirty",
		16 => "robot_tilted",
		17 => "side_brush_error",
		18 => "fan_error",
		19 => "Dock not connected to power",
		20 => "optical_flow_sensor_dirt",
		21 => "vertical_bumper_pressed",
		22 => "dock_locator_error",
		23 => "return_to_dock_fail",
		24 => "nogo_zone_detected",
		25 => "Camera error",
		26 => "Wall sensor error",
		27 => "vibrarise_jammed",
		28 => "robot_on_carpet",
		29 => "filter_blocked",
		30 => "invisible_wall_detected",
		31 => "cannot_cross_carpet",
		32 => "internal_error",
		34 => "Clean auto-empty dock",
		35 => "Auto empty dock voltage error",
		36 => "Wash roller may be jammed",
		37 => "wash roller not lowered properly",
		38 => "Check the clean water tank",
		39 => "Check the dirty water tank",
		40 => "Reinstall the water filter",
		41 => "Clean water tank empty",
		42 => "Check that the water filter has been correctly installed",
		43 => "Positioning button error",
		44 => "Clean the dock water filter",
		45 => "Wash roller may be jammed",
		48 => "up_water_exception",
		49 => "drain_water_exception",
		51 => "Unit temperature protection",
		52 => "clean_carousel_exception",
		53 => "clean_carousel_water_full",
		54 => "water_carriage_drop",
		55 => "check_clean_carouse",
		56 => "audio_error",
	);

	foreach($error_code_traduction as $key => $value) {
    	if ($key == $json_decode['error_code']) {
        	cmd::byString('#[Technique][Roborock Alita][Code Erreur 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, 'result":[');
  	$output = strstr($output, '{');
  	$output = str_replace('\\','', $output);
  	$json = strstr($output, ']}', true) .']}]}';
	//$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é

	// 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]);
          	$scene_correspondance[$id] = preg_replace('/\\\\u([\da-fA-F]{4})/', '&#x\1;', $scene_correspondance[$id]);
          	$scene_correspondance[$id] = html_entity_decode($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");

  	$scenario->setLog('Modification water box custom mode :' ."\n" .$water_mop ."\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");

      	$scenario->setLog('Modification mode mop :' ."\n" .$mode_mop ."\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");

    $scenario->setLog('Modification puissance aspiration :' ."\n" .$puissance_aspiration ."\n");

  
  	// On lance le nettoyage par segment avec l'id de la pièce
  
    if ($tags['#repetition#'] != '') {
        $repetition = $tags['#repetition#'];
    } else {
    	$repetition = $repetition_defaut;
    }
   	
  	// Lancement 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).

EDIT : 13/08/2025 : mise à jour du bloc-code pour prendre en compte les modification apportées à la librairie python-roborock (commande status, changement du retour de la commande status, changement de noms et suppression de certaines données du json)

02/01/2026 : MAJ du bloc-code pour compatibilité PHP8 et librairie python-roborock 4.2.0.
MAJ de la méthode de connexion à son compte Roborock.

08/01/2026 : MAJ fonction « room_list » en cas d’apostrophe dans le nom de pièce.
MAJ fonction « scene_list » pour tenir compte des caractères spéciaux dans le nom des routines.
MAJ fonction "status’ pour tenir compte d’un json vide

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:

1 « J'aime »

Bonjour,

Mise à jour du bloc-code suite aux modifications apportées à la librairie python-roborock.

Merci pour ça ! Je suis surpris que tous les roborocks ne soient pas déjà compatible jeedom a 100% :wink:
Ta solution me paraît en effet très bidouillé, moi qui possède une box atlas, je ne sais même pas si je peux faire tout ça… l’IA n’a pas réussi a créer un plugin de toute piece ?

1 « J'aime »

Bonjour,

Aucune réelle bidouille.
Le scénario avec le bloc code fait l’interface avec la librairie (avec l’aide de plugin-sshmanager) et ensuite les commandes sont réalisées avec des tags passés à ce scénario par l’intermédiaire d’une second scénario. Donc c’est assez propre.

Dans ton cas avec une Atlas le problème est l’installation de la librairie python. Je déconseilles dans ton cas son installation sur la même machine. Mais un Pi02W devrait faire l’affaire.

Et si l’IA savait tout programmer alors il n’y aurait déjà plus de développeurs :wink:

Edit : tes vœux sont probablement exaucés :

1 « J'aime »

Bonjour,

Je ne sais pas quel Roborocks vous avez mais je sais que @thanaus prépare un plugin pour certains modèle de Roborocks :wink:

3 « J'aime »

Salut,

Oui merci ! :+1:
Je viens de le voir et j’ai modifié mon post.

A priori c’est sur la base de la même bibliothèque python (il n’y en a pas d’autre maintenue de toute façon).
Donc tout modèle Roborock.

C’est une super nouvelle surtout vu la qualité du travail de @thanaus :blush:

4 « J'aime »

A tout hasard… Je possède un Dreame (donc pas un Roborock), qui fonctionnait avec plugin-mirobot mais qui ne fonctionne plus depuis un changement sur l’api Xiaomi en juillet.
Cette librairie ne semble fonctionner que pour des aspirateurs Roborock d’après cette page, mais je me demandais si certain avait essayé…
J’ai bien peur aussi que le plugin #roborock en préparation par @thanaus , ne soit aussi exclusifs aux Roborock… Sur Jeedom il n’y a, à ma connaissance plus de solution fonctionnelle via plugin.

Je me satisferais d’une solution bidouillée, donc si un bidouilleur possesseur d’un Dreame passe par là… Merci à tous !

Bonjour,

Je pense que le nouveau plugin s’appuie sur la même bibliothèque python que moi, donc exclusive aux Roborock.

Par contre tu peux essayer directement avec la librairie python Xiaomi ?

Bonjour,

Attention il semble que la communication locale n’existe plus sur les nouveaux modèles de type Q10 ou Q7. Donc ils seraient 100 % cloud.

Ainsi pas possible d’utiliser la bibliothèque python pour eux. Cela devrait être également le cas pour le futur plugin de @thanaus :roll_eyes:

Le plugin est dispo sur le market ? Je serais interessé …
Meme en version alfa ( que je ne trouve pas pour le moment oO , mais le souci doit etre entre la chaise et le clavier :wink: ).

J’ia un QV 35A que j’aimerais jeedomiser :wink:

Bonjour,

Encore rien vu passer de son côté.
En même temps dernièrement il y a eu pas mal de modif sur la librairie. Il attend peut-être une stabilisation.

Sinon il faut suivre ma bidouille.

Yes , merci, j’ai encore pas pris le temps de regarder avec soins ta solution, le cote plugin, est super adapté au faignant que je suis ;).

Merci @Madcow :star_struck:

Installation de python-roborock 3.7.1 via pipx en local, cohabitation jeedom, python 3.11 environnement virtuel dédié (venv) location: /opt/roborock-venv/lib/python3.11/site-packages

Voici le code que j’utilise. Pas de gestion de piece car je passe par la fonction native de google home

// ============================================================================
// Pilote Roborock S8 / S8 Pro Ultra via Jeedom + SSH Manager + python-roborock
// Version complète avec toutes les commandes ACTION
// ============================================================================


// ============================================================================
// 1) RÉCUPÉRATION DES TAGS
// ============================================================================

$tags = $scenario->getTags();
$scenario->setLog('Tags reçus : ' . print_r($tags, true));


// ============================================================================
// 2) VARIABLES À PERSONNALISER
// ============================================================================

$sshManagerEqLogic = '#[Jeedom][SSH Roborock S8]#';
$device_id = 'bqyI21awQoIeZ8ZhaaKym';
$virtuel = '#[Maison][Roborock S8]';

$water_mop_defaut = 202;     // medium
$puissance_aspiration_defaut = 102; // balanced
$mode_mop_defaut = 300;      // standard
$repetition_defaut = 1;      // standard


// ============================================================================
// 3) ID SSH MANAGER
// ============================================================================

$sshManagerId = eqLogic::byString($sshManagerEqLogic)->getId();

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


// ============================================================================
// 4) COMMANDES DIRECTES EN FRANÇAIS (VERSION COMPATIBLE python-roborock 3.7.1)
// ============================================================================

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

// ===== DÉMARRER =====
if ($commande_interne == "demarrer") {
    sshmanager::executeCmds($sshManagerId,
        "/opt/roborock-venv/bin/roborock command --device_id $device_id --cmd app_start"
    );
    $scenario->setLog("Commande : Démarrer");
    return;
}

// ===== PAUSE =====
if ($commande_interne == "pause") {
    sshmanager::executeCmds($sshManagerId,
        "/opt/roborock-venv/bin/roborock command --device_id $device_id --cmd app_pause"
    );
    $scenario->setLog("Commande : Pause");
    return;
}

// ===== REPRENDRE =====
if ($commande_interne == "reprendre") {
    sshmanager::executeCmds($sshManagerId,
        "/opt/roborock-venv/bin/roborock command --device_id $device_id --cmd app_resume"
    );
    $scenario->setLog("Commande : Reprendre");
    return;
}

// ===== ARRÊTER =====
if ($commande_interne == "arreter") {
    sshmanager::executeCmds($sshManagerId,
        "/opt/roborock-venv/bin/roborock command --device_id $device_id --cmd app_stop"
    );
    $scenario->setLog("Commande : Arrêter");
    return;
}

// ===== RETOUR À LA BASE =====
if ($commande_interne == "base") {
    sshmanager::executeCmds($sshManagerId,
        "/opt/roborock-venv/bin/roborock command --device_id $device_id --cmd app_charge"
    );
    $scenario->setLog("Commande : Retour à la base");
    return;
}

// ===== TROUVER LE ROBOT =====
if ($commande_interne == "trouver") {
    sshmanager::executeCmds($sshManagerId,
        "/opt/roborock-venv/bin/roborock command --device_id $device_id --cmd find_me"
    );
    $scenario->setLog("Commande : Trouver le robot");
    return;
}

// ===== NETTOYER LA STATION =====
// Appel du cycle de lavage (pas de commande `dock-clean` dans ta version)
if ($commande_interne == "nettoyage_station") {
    sshmanager::executeCmds($sshManagerId,
        "/opt/roborock-venv/bin/roborock command --device_id $device_id --cmd app_start_wash"
    );
    $scenario->setLog("Commande : Nettoyage/lavage station");
    return;
}

// ===== SÉCHAGE SERPILLIÈRE =====
if ($commande_interne == "sechage") {
    sshmanager::executeCmds($sshManagerId,
        "/opt/roborock-venv/bin/roborock command --device_id $device_id --cmd set_airdry_hours --params '[\"1\"]'"
    );
    $scenario->setLog("Commande : Séchage serpillière");
    return;
}



// ============================================================================
// 5) COMMANDE : STATUS
// ============================================================================

if ($commande == 'status') {

    $cmdSSH = "/opt/roborock-venv/bin/roborock command --device_id $device_id --cmd get_status";
    $output = sshmanager::executeCmds($sshManagerId, $cmdSSH);

    // Extraction JSON
    $lines = preg_split("/\r\n|\n|\r/", trim($output));
    $json = "";

    for ($i = count($lines)-1; $i >= 0; $i--) {
        $l = trim($lines[$i]);
        if ($l !== "" && ($l[0] === "[" || $l[0] === "{")) {
            $json = $l;
            break;
        }
    }

    if ($json == "") {
        $scenario->setLog("ERREUR : JSON introuvable.\n$output");
        return;
    }

    $data = json_decode($json, true);

    if ($data === null) {
        $scenario->setLog("ERREUR JSON : ".json_last_error_msg()."\n$json");
        return;
    }

    $s = isset($data[0]) ? $data[0] : $data;


    // Mise à jour virtuel
    cmd::byString($virtuel.'[Status Robot Value]#')->event($s['state']);
    cmd::byString($virtuel.'[Batterie]#')->event($s['battery']);
    cmd::byString($virtuel.'[Code Erreur Value]#')->event($s['error_code']);
    cmd::byString($virtuel.'[Code Erreur Dock Value]#')->event($s['dock_error_status']);
    cmd::byString($virtuel.'[Eau Mop Value]#')->event($s['water_box_mode']);
    cmd::byString($virtuel.'[Mode Mop Value]#')->event($s['mop_mode']);
    cmd::byString($virtuel.'[Puissance Aspiration Value]#')->event($s['fan_power']);
    cmd::byString($virtuel.'[Séchage Mop en cours]#')->event($s['dry_status']);
    cmd::byString($virtuel.'[Temps Séchage Mop Restant]#')->event($s['rdt']);
    cmd::byString($virtuel.'[Mode Nettoyage]#')->event($s['in_cleaning']);
    cmd::byString($virtuel.'[Superficie Dernier Nettoyage]#')->event($s['clean_area']);
    cmd::byString($virtuel.'[Durée Dernier Nettoyage]#')->event($s['clean_time']);
    cmd::byString($virtuel.'[Pourcentage Nettoyage Réalisé]#')->event($s['clean_percent']);
  	cmd::byString($virtuel.'[Date]#')->event(date('Y-m-d H:i:s'));

    // --- Traductions lisibles ---
    $map_dock_error = [
        0=>"Aucune erreur",34=>"Conduit obstrué",38=>"Réservoir eau propre vide",
        39=>"Réservoir eau sale plein",42=>"Brosse de maintenance bloquée",
        44=>"Couvercle bac sale ouvert",46=>"Bac à poussière absent",
        53=>"Bac nettoyage bloqué/plein"
    ];

    $map_fan_power = [
        105=>"Arrêt",101=>"Silencieux",102=>"Équilibré",103=>"Turbo",
        104=>"Max",106=>"Personnalisé",108=>"Max Plus",110=>"Mode intelligent"
    ];

    $map_state = [
        0=>"Inconnu",1=>"Démarrage",2=>"Chargeur débranché",3=>"Inactif",
        4=>"Contrôle manuel",5=>"Nettoyage",6=>"Retour base",7=>"Mode manuel",
        8=>"En charge",9=>"Problème charge",10=>"Pause",11=>"Spot cleaning",
        12=>"Erreur",13=>"Extinction",14=>"Mise à jour",15=>"Docking",
        16=>"Aller vers cible",17=>"Zone",18=>"Segment",
        22=>"Vidage du bac",23=>"Lavage mop",25=>"Lavage mop 2",
        26=>"Va laver mop",28=>"En appel",29=>"Cartographie",
        30=>"Attaque d'œuf",32=>"Patrouille",33=>"Fixe mop",
        34=>"Retire mop",100=>"Chargé",101=>"Offline",103=>"Verrouillé",
        202=>"Arrêt séchage",6301=>"Serpillière",6302=>"Lavage serpillière",
        6303=>"Serpillière en cours",6304=>"Segment mop"
    ];

    $map_water = [
        200=>"Off",201=>"Faible",202=>"Moyen",203=>"Élevé",
        204=>"Personnalisé",207=>"Débit personnalisé",209=>"Smart"
    ];

    $map_mop_mode = [
        300=>"Standard",301=>"Profond",302=>"Custom",303=>"Très profond",
        304=>"Rapide",306=>"Smart"
    ];

    $map_error_code = [
        0=>"Aucune erreur",1=>"Lidar bloqué",2=>"Pare-chocs coincé",
        3=>"Roue suspendue",4=>"Capteur vide",5=>"Brosse principale bloquée",
        6=>"Brosse latérale bloquée",7=>"Roues bloquées",8=>"Robot bloqué",
        9=>"Bac absent"
    ];


    // Injection texte
    if (isset($map_dock_error[$s['dock_error_status']]))
        cmd::byString($virtuel.'[Code Erreur Dock]#')->event($map_dock_error[$s['dock_error_status']]);

    if (isset($map_fan_power[$s['fan_power']]))
        cmd::byString($virtuel.'[Puissance Aspiration]#')->event($map_fan_power[$s['fan_power']]);

    if (isset($map_state[$s['state']])) {

    // Cas spécial : robot sur base + batterie à 100 %
    if ($s['state'] == 8 && $s['battery'] == 100) {
        cmd::byString($virtuel.'[Status Robot]#')->event("Chargé");
    } else {
        cmd::byString($virtuel.'[Status Robot]#')->event($map_state[$s['state']]);
    }

	}

    if (isset($map_water[$s['water_box_mode']]))
        cmd::byString($virtuel.'[Eau Mop]#')->event($map_water[$s['water_box_mode']]);

    if (isset($map_mop_mode[$s['mop_mode']]))
        cmd::byString($virtuel.'[Mode Mop]#')->event($map_mop_mode[$s['mop_mode']]);

    if (isset($map_error_code[$s['error_code']]))
        cmd::byString($virtuel.'[Code Erreur]#')->event($map_error_code[$s['error_code']]);
}



// ============================================================================
// 6) SCENE_LIST : Liste des routines
// ============================================================================

if ($commande == 'scene_list') {

    $cmdSSH = "/opt/roborock-venv/bin/roborock list-scenes --device_id $device_id";
    $output = sshmanager::executeCmds($sshManagerId, $cmdSSH);

    $txt = substr($output, 5, strlen($output)-8);
    $rows = explode(',', $txt);

    $scene_correspondance = [];

    foreach ($rows as $k => $v) {
        if (strpos($v, "id") !== false) {
            $id = substr($v, strpos($v, ": ") + 2);
            $name = substr($rows[$k+1], strpos($rows[$k+1], ": ") + 2);
            $name = trim(str_replace(["}","\""], "", $name));
            $scene_correspondance[$id] = $name;
        }
    }

    $scenario->setLog("Routines :\n".print_r($scene_correspondance, true));
}





// ============================================================================
// 7) SCENE : Lancer une routine Roborock
// ============================================================================

if ($commande == 'scene') {

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

    if ($scene_id == '') {
        $scenario->setLog("ERREUR : aucune scène définie.");
        return;
    }

    $cmdSSH = "/opt/roborock-venv/bin/roborock execute-scene --scene_id $scene_id";
    sshmanager::executeCmds($sshManagerId, $cmdSSH);

    $scenario->setLog("Routine lancée : ID $scene_id");
}

Actualisation toutes les 12 minutes et passage à toutes les minutes en cours de nettoyage.

:white_check_mark: COMMANDES UTILISABLES POUR LE ROBOROCK S8 PRO ULTRA

:white_check_mark: 1) Commandes de nettoyage

Commande Action
app_start Démarrer nettoyage complet
app_stop Arrêter nettoyage
app_pause Pause
app_resume Reprendre
app_segment_clean Nettoyage par pièce
app_zoned_clean Nettoyage par zone
resume_segment_clean Reprise nettoyage pièce
resume_zoned_clean Reprise nettoyage zone
app_spot Nettoyage spot
app_start_build_map Démarrer construction de carte
app_resume_build_map Reprendre construction carte

:white_check_mark: 2) Gestion de la base et batteries

Commande Action
app_charge Retour à la base et charge
app_start_wash_then_charge Laver serpillière puis rentrer charger
app_start_collect_dust Déclenche la vidange automatique
app_stop_collect_dust Arrête vidange (rarement utilisé)

:white_check_mark: 3) Gestion serpillière / mop

Commande Action
set_mop_mode Mode serpillière (300,301,302…)
set_water_box_custom_mode Niveau eau (200,201,202…)
app_start_wash Nettoyage serpillière
app_stop_wash Stop lavage serpillière
set_airdry_hours Activer séchage (1 à 4 h)
set_mop_motor_status Allumer/éteindre moteur mop
mop_mode Fixer mode mop (rare)

:white_check_mark: 4) Navigation / mouvement

Commande Action
app_goto_target Aller à une coordonnée X,Y
stop_goto_target Stop déplacement

:white_check_mark: 5) Remote control (mode manuel)

Commande Action
app_rc_start Démarrer télécommande
app_rc_move Déplacer (paramètres X/Y)
app_rc_stop Stop mouvement
app_rc_end Fin télécommande

:white_check_mark: 6) Actions diverses

Commande Action
find_me Faire crier le robot
reset_consumable Reset consommables
set_voice_chat_volume Volume
test_sound_volume Test son
1 « J'aime »

Bonjour,j essaye de configurer la connexion a mon saro 10 via la méthode décrit plus mais lors de la connexion ont me demande le code a 2 facteurs alors que ça n’existe pas dans l’application roborock.Quelqu’un a déjà u ce soucis ?

Bonjour,

Je crois que le code est envoyé sur ton adresse mail.

Des fois les serveurs d’identification bugguent :wink:

Ok je vais essayer merci