Recette agent · roborock-v1-vacuum
Roborock V1 vacuum
Drive a Roborock robot vacuum that speaks the V1 protocol (Q8 Max, S7, S8 and kin) with python-roborock - read its status and rooms, send it to a point without cleaning, clean a room, remote-control it, bring it back to its dock.
Lancée par son auteur, pas encore reproduite- Pilotage local
- Une connexion au cloud, une seule fois
- Il bouge : l'agent demande avant
- Testé sur
- Q8 Max+ SE (Q80ULL) · firmware 02.06.86
- 9 actions
- vacuum.start
- vacuum.pause
- vacuum.return_to_base
- vacuum.clean_spot
- vacuum.clean_area
- vacuum.locate
- vacuum.set_fan_speed
- vacuum.send_command
- roborock.set_vacuum_goto_position
Control a Roborock robot vacuum from an agent, with the community library python-roborock (the same one Home Assistant's Roborock integration uses). Everything below was run on a real Q8 Max+ SE on 14/10/2026 unless marked "not tried".
Rules before anything
- The robot really moves. Before every movement command, tell the owner what it will do and get a yes. Check the floor is clear (cables, socks, pets) and respect quiet hours.
- Never print, copy or send
~/.roborock(the cached session), the device id (duid), the local key, the serial number, the robot's IP address or the Wi-Fi name. Keep the duid in a shell variable; leave identifiers out of any report. - Note every setting you change (fan power, volume, auto-empty) and restore it when done.
Identify the device
- Ask for the model on the label under the robot, or read it after login: python-roborock reports a model id such as
roborock.vacuum.a73. - This recipe is for the V1 protocol only: in python-roborock,
device.v1_properties is not None. A01 devices (Dyad, Zeo) and B01 devices (Q7, Q10) need another recipe. Do not pick this recipe from the brand name alone. - Network clues before login: a DHCP hostname like
roborock-vacuum-a73(the end is the model id), a MAC address starting withB0:4A:39or24:9E:7D(Home Assistant's matchers). The robot announces nothing over mDNS or SSDP.
Prerequisites
- Python 3.11 or later and
uv. Below,rrstands foruvx --from 'python-roborock[cli]' roborock. - One login, done by the owner in their own terminal (the account is theirs, the code comes by email):
uvx --from 'python-roborock[cli]' roborock login --email <their email>Never use--password: it lands in the shell history and in the agent's context. The session is cached in~/.roborockand reused by every later command. - The device id, kept in a variable and never echoed:
DUID=$(rr list-devices | python3 -c 'import json,sys; print(next(iter(json.load(sys.stdin).values())))')(list-devicesprints{name: duid}; pick by name if there are several robots.) - The agent's machine on the same local network as the robot, so the library can use the local channel (TCP 58867).
Workflow
Read
| What | Command | Notes |
|---|---|---|
| Status | rr command --device_id "$DUID" --cmd get_status |
Raw keys in snake_case (state, battery, fan_power, map_status, error_code, in_cleaning). rr status gives the same in camelCase |
| Rooms | rr rooms --device_id "$DUID" |
segmentId and the name given in the Roborock app. Empty if rooms were never named, or if the robot is on a temporary map (see Known traps) |
| Dock and robot position | rr map-data --device_id "$DUID" |
charger {x, y, a} and vacuum_position {x, y, a}, millimetres in the map frame, angle in degrees |
| Saved maps | rr command --device_id "$DUID" --cmd get_multi_maps_list |
mapFlag of each saved map |
State codes (python-roborock RoborockStateCode): 2 charger disconnected (off the dock), 3 idle, 5 cleaning, 6 returning home, 7 manual mode (remote control), 8 charging, 10 paused, 11 spot cleaning, 12 error, 15 docking, 16 going to target, 18 room cleaning, 22 emptying the bin, 100 charging complete.
Act
Raw V1 commands: rr command --device_id "$DUID" --cmd <command> [--params '<json>']. The names come from python-roborock's RoborockCommand enum (roborock/roborock_typing.py); look others up there.
| Goal | Command and params | Tried on 14/10/2026 |
|---|---|---|
| Go to a point without cleaning | app_goto_target [x, y] |
Yes: state 16, then 3, stopped 59 mm from the target |
| Back to the dock | app_charge |
Yes: state 6, then 8, about 30 s from 1 m away |
| Load a saved map | load_multi_map [mapFlag] |
Yes, robot on its dock |
| Fan power | set_custom_mode [101] to [104] (quiet, balanced, turbo, max on this model) |
Yes |
| Volume | change_sound_volume [0..100], read with get_sound_volume |
Yes |
| Auto-empty when docking | set_dust_collection_switch_status {"status": 0} or 1 |
Yes |
| Clean rooms | app_segment_clean [{"segments": [16], "repeat": 1}] (older firmwares take [16]) |
Not tried |
| Full clean, pause, spot | app_start, app_pause, app_spot |
Not tried |
| "I'm here" sound | find_me |
Not tried |
Going to a point. A safe first target is in front of the dock: x = charger.x + d * cos(a), y = charger.y + d * sin(a) with d around 800 to 1000 mm. On 14/10/2026 (a = -180) a target 1 m out was reached in under a minute. Choose open floor. Not known yet: whether the fan runs during the trip when it is not on quiet, so set set_custom_mode [101] first if the owner wants silence, and restore it after.
Remote control needs a long-lived session, not one CLI call per move:
app_rc_start, then wait about 1.5 s.app_rc_moveevery 250 ms with[{"omega": <rad/s>, "velocity": <m/s>, "seqnum": <n>, "duration": 600}].velocitystays within plus or minus 0.3 m/s;omegaup to about 2 rad/s worked;seqnumgoes up by one each time. The 600 ms duration is the dead man's switch: if the agent dies, the robot stops by itself.- A last move at zero, then
app_rc_end. The robot reports state 7 while driven.
# Run: uv run --no-project --with 'python-roborock[cli]' drive.py
import asyncio
from roborock.cli import RoborockContext # reuses the CLI login cache; internal API, checked on 7.12.3
async def main() -> None:
manager = await RoborockContext().get_device_manager()
await manager.ensure_device_manager()
try:
device = next(d for d in await manager.device_manager.get_devices() if d.v1_properties)
send = device.v1_properties.command.send
await send("app_rc_start")
try:
await asyncio.sleep(1.5)
for seq in range(1, 9): # about 2 s forward, slowly
await send("app_rc_move", [{"omega": 0.0, "velocity": 0.1, "seqnum": seq, "duration": 600}])
await asyncio.sleep(0.25)
await send("app_rc_move", [{"omega": 0.0, "velocity": 0.0, "seqnum": 9, "duration": 600}])
finally:
await send("app_rc_end")
await asyncio.sleep(1.5) # see Known traps, 1
finally:
await manager.device_manager.close()
asyncio.run(main())
Verify the result
["ok"]only means the robot accepted the command. Pollget_statusevery 1 to 2 s and check the state moves as expected: go to a point, 16 then 3; return, 6 then 8; room cleaning, 18.- Returning is not docked. Docked is state 8 or 100. State 2 means off the dock.
- After any movement session, check that
map_status >> 2is not 63 and thatrr roomsis not empty. - Tell the owner what moved, where the robot is now, and which settings were restored.
Known traps
Each one cost time on the real robot on 14/10/2026 (6 to 8 on 16/10/2026).
app_chargeright afterapp_rc_endis ignored. The robot answers["ok"]and stays idle (state 3) off its dock. Wait 1.5 s afterapp_rc_end, check for state 6 within 5 s, and resend once if it is not there.- A remote-control session can leave the robot on a temporary map. Seen once in two sessions:
map_status253 (253 >> 2 = 63, "no map"), rooms empty, dock position wrong. The saved map was still listed byget_multi_maps_list. Fix: with the robot on its dock,load_multi_map [<mapFlag of the saved map>](0 when there is one map);map_statuswent back to 3 and the rooms came back. - A driven robot is left where it stopped. After remote control, ending the session does not send it home: say so to the owner, and send
app_chargeonly with their yes. - Positions move by about 10 cm between two reads of the map, so expect about 10 cm precision.
- The robot's voice cannot be replaced. Since the third generation (2019), Roborock robots install only voice packs signed by Roborock, and no command plays arbitrary audio or text. To make the robot talk, use a speaker riding on it (Bluetooth from the computer) that does not cover the laser turret on top, and mind the extra height under low furniture.
- A robot whose dock was unplugged goes silent. Its battery runs down and it drops off Wi-Fi: no ARP answer, no ping, local port closed, and the cloud command times out after 10 s. Nothing to redo in the app: plug the dock back in, set the robot on it, and it rejoins Wi-Fi on its own. Tell the owner to keep the dock always powered.
- Neither
state3 norcharge_statustells it is charging.charge_statusstayed 1 (charging) for 25 minutes whilestatewas 2 (charger_disconnected) and the battery fell from 77 to 75; before that,state3 withcharge_status1 and the battery stuck at 77. Onlystate8 (charging) or 100 (charging complete) means the robot sits on powered contacts, or a battery that rises between two reads a few minutes apart. app_start_collect_dustanswers["ok"]even when the dock cannot empty. With the robot off its charging contacts (state2), the dock stayed silent anddust_collection_statusstayed 0. Checkstate8 or 100 before sending it, then expectstate22 (emptying the bin) within a few seconds; no 22 means it did not run. The do-not-disturb timer also blocks emptying when itsactions.dustis 1 (get_dnd_timer).
Limits
- Verified on one robot only: Q8 Max+ SE (Q80ULL,
roborock.vacuum.a73), firmware 02.06.86, python-roborock 7.12.3. Other V1 models listed by python-roborock (S4 to S8, Q5, Q7 Max, Q8 Max, Q Revo) are likely close but were not tried with this recipe. - Not cloud-free: the login goes through Roborock's cloud and the library keeps a cloud MQTT session next to the local channel. For a cloud-free setup, see local_roborock_server (DNS redirection, own domain and certificate). Valetudo does not support the Q8 Max.
- The remote-control snippet uses
roborock.cli.RoborockContext, an internal API that may change between versions; the documented path iscreate_device_manager(UserParams(...))after a login withRoborockApiClient.
Sources
- python-roborock, version 7.12.3 (14/10/2026): https://github.com/Python-roborock/python-roborock, command names in
roborock/roborock_typing.py, state codes inroborock/data/v1/v1_code_mappings.py, map flag inroborock/data/v1/v1_containers.py(current_map). - Home Assistant Roborock integration, dev branch (14/10/2026): DHCP matchers in
manifest.json,set_vacuum_goto_positioninservices.yaml; vacuum services (clean_areaand others) inhomeassistant/components/vacuum/services.yaml. - Signed voice packs: https://github.com/andreypopov/node-red-contrib-miio-roborock/issues/18 (2020).
- Local channel on TCP 58867: https://github.com/Python-roborock/python-roborock/blob/HEAD/roborock/devices/transport/local_channel.py
- Runs on a real robot: the author's verification notes of 14/10/2026.
Pour ton agent
La même recette en Markdown brut : donne ce fichier à lire à ton agent.
https://hardhub.dev/recipes/roborock-v1-vacuum/SKILL.md