Aller au contenu
Hard Hub
Retour à Aspirateurs robots Roborock

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 with B0:4A:39 or 24:9E:7D (Home Assistant's matchers). The robot announces nothing over mDNS or SSDP.

Prerequisites

  • Python 3.11 or later and uv. Below, rr stands for uvx --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 ~/.roborock and 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-devices prints {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:

  1. app_rc_start, then wait about 1.5 s.
  2. app_rc_move every 250 ms with [{"omega": <rad/s>, "velocity": <m/s>, "seqnum": <n>, "duration": 600}]. velocity stays within plus or minus 0.3 m/s; omega up to about 2 rad/s worked; seqnum goes up by one each time. The 600 ms duration is the dead man's switch: if the agent dies, the robot stops by itself.
  3. 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. Poll get_status every 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 >> 2 is not 63 and that rr rooms is 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).

  1. app_charge right after app_rc_end is ignored. The robot answers ["ok"] and stays idle (state 3) off its dock. Wait 1.5 s after app_rc_end, check for state 6 within 5 s, and resend once if it is not there.
  2. A remote-control session can leave the robot on a temporary map. Seen once in two sessions: map_status 253 (253 >> 2 = 63, "no map"), rooms empty, dock position wrong. The saved map was still listed by get_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_status went back to 3 and the rooms came back.
  3. 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_charge only with their yes.
  4. Positions move by about 10 cm between two reads of the map, so expect about 10 cm precision.
  5. 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.
  6. 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.
  7. Neither state 3 nor charge_status tells it is charging. charge_status stayed 1 (charging) for 25 minutes while state was 2 (charger_disconnected) and the battery fell from 77 to 75; before that, state 3 with charge_status 1 and the battery stuck at 77. Only state 8 (charging) or 100 (charging complete) means the robot sits on powered contacts, or a battery that rises between two reads a few minutes apart.
  8. app_start_collect_dust answers ["ok"] even when the dock cannot empty. With the robot off its charging contacts (state 2), the dock stayed silent and dust_collection_status stayed 0. Check state 8 or 100 before sending it, then expect state 22 (emptying the bin) within a few seconds; no 22 means it did not run. The do-not-disturb timer also blocks emptying when its actions.dust is 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 is create_device_manager(UserParams(...)) after a login with RoborockApiClient.

Sources

Pour ton agent

La même recette en Markdown brut : donne ce fichier à lire à ton agent.

SKILL.md brut

https://hardhub.dev/recipes/roborock-v1-vacuum/SKILL.md