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
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




