Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .claude/Screenshot 2026-07-14 at 13.22.14.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
182 changes: 182 additions & 0 deletions .claude/roborock_robot_calls_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Robot command inventory — python-roborock-main

Repository scanned: `roborock/` source tree (tests excluded for usage counts).

## 1. V1 raw command catalog (253 commands)

These are string RPC commands accepted by `device.v1_properties.command.send(command, params)`. Availability and parameter formats depend on robot model and firmware.

### App / robot actions (50)

`app_amethyst_self_check` · `app_charge` · `app_delete_wifi` · `app_get_amethyst_status` · `app_get_carpet_deep_clean_status` · `app_get_clean_estimate_info`

`app_get_dryer_setting` · `app_get_init_status` · `app_get_locale` · `app_get_wifi_list` · `app_goto_target` · `app_keep_easter_egg`

`app_pause` · `app_rc_end` · `app_rc_move` · `app_rc_start` · `app_rc_stop` · `app_resume_build_map`

`app_resume_patrol` · `app_segment_clean` · `app_set_amethyst_status` · `app_set_carpet_deep_clean_status` · `app_set_cross_carpet_cleaning_status` · `app_set_door_sill_blocks`

`app_set_dirty_replenish_clean_status` · `app_set_dryer_setting` · `app_set_dryer_status` · `app_set_dynamic_config` · `app_set_ignore_stuck_point` · `app_set_smart_cliff_forbidden`

`app_set_smart_door_sill` · `app_spot` · `app_start` · `app_start_build_map` · `app_start_collect_dust` · `app_start_easter_egg`

`app_start_patrol` · `app_start_pet_patrol` · `app_start_wash` · `app_stat` · `app_stop` · `app_stop_collect_dust`

`app_stop_wash` · `app_update_unsave_map` · `app_wakeup_robot` · `app_zoned_clean` · `app_empty_rinse_tank_water` · `app_ignore_dirty_objects`

`app_set_robot_setting` · `app_get_robot_setting`

### Read / query (79)

`get_auto_delivery_cleaning_fluid` · `get_camera_status` · `get_carpet_clean_mode` · `get_carpet_mode` · `get_child_lock_status` · `get_clean_follow_ground_material_status`

`get_clean_motor_mode` · `get_clean_record` · `get_clean_record_map` · `get_clean_sequence` · `get_clean_summary` · `get_collision_avoid_status`

`get_consumable` · `get_current_sound` · `get_custom_mode` · `get_customize_clean_mode` · `get_device_ice` · `get_device_sdp`

`get_dnd_timer` · `get_dock_info` · `get_dust_collection_mode` · `get_dust_collection_switch_status` · `get_dynamic_data` · `get_dynamic_map_diff`

`get_fan_motor_work_timeout` · `get_flow_led_status` · `get_fresh_map` · `get_fw_features` · `get_homesec_connect_status` · `get_identify_furniture_status`

`get_identify_ground_material_status` · `get_led_status` · `get_log_upload_status` · `get_map` · `get_map_beautification_status` · `get_map_status`

`get_map_v1` · `get_map_v2` · `get_map_calibration` · `get_mop_motor_status` · `get_mop_template_params_by_id` · `get_mop_template_params_summary`

`get_multi_map` · `get_multi_maps_list` · `get_network_info` · `get_offline_map_status` · `get_persist_map` · `get_prop`

`get_random_pkey` · `get_recover_map` · `get_recover_maps` · `get_room_mapping` · `get_scenes_valid_tids` · `get_segment_status`

`get_serial_number` · `get_server_timer` · `get_smart_wash_params` · `get_sound_progress` · `get_sound_volume` · `get_status`

`get_testid` · `get_timer` · `get_timer_detail` · `get_timer_summary` · `get_timezone` · `get_turn_server`

`get_valley_electricity_timer` · `get_wash_debug_params` · `get_wash_towel_mode` · `get_wash_towel_params` · `get_water_box_custom_mode` · `get_stretch_tag_status`

`get_right_brush_stretch_status` · `get_dirty_object_detect_status` · `get_wash_water_temperature` · `get_pet_supplies_deep_clean_status` · `get_ap_mic_led_status` · `get_handle_leak_water_status`

`get_gap_deep_clean_status`

### Configuration (57)

`set_airdry_hours` · `set_app_timezone` · `set_auto_delivery_cleaning_fluid` · `set_camera_status` · `set_carpet_area` · `set_carpet_clean_mode`

`set_carpet_mode` · `set_child_lock_status` · `set_clean_follow_ground_material_status` · `set_clean_motor_mode` · `set_clean_sequence` · `set_clean_repeat_times`

`set_collision_avoid_status` · `set_custom_mode` · `set_customize_clean_mode` · `set_dnd_timer` · `set_dnd_timer_actions` · `set_dust_collection_mode`

`set_dust_collection_switch_status` · `set_fan_motor_work_timeout` · `set_fds_endpoint` · `set_flow_led_status` · `set_homesec_password` · `set_identify_furniture_status`

`set_identify_ground_material_status` · `set_ignore_carpet_zone` · `set_ignore_identify_area` · `set_lab_status` · `set_led_status` · `set_map_beautification_status`

`set_mop_mode` · `set_mop_motor_status` · `set_mop_template_id` · `set_offline_map_status` · `set_scenes_segments` · `set_scenes_zones`

`set_segment_ground_material` · `set_server_timer` · `set_smart_wash_params` · `set_switch_map_mode` · `set_timer` · `set_timezone`

`set_valley_electricity_timer` · `set_voice_chat_volume` · `set_wash_debug_params` · `set_wash_towel_mode` · `set_wash_towel_params` · `set_water_box_custom_mode`

`set_water_box_distance_off` · `set_stretch_tag_status` · `set_right_brush_stretch_status` · `set_dirty_object_detect_status` · `set_wash_water_temperature` · `set_pet_supplies_deep_clean_status`

`set_ap_mic_led_status` · `set_handle_leak_water_status` · `set_gap_deep_clean_status`

### Start actions (5)

`start_camera_preview` · `start_clean` · `start_edit_map` · `start_voice_chat` · `start_wash_then_charge`

### Stop actions (6)

`stop_camera_preview` · `stop_fan_motor_work` · `stop_goto_target` · `stop_segment_clean` · `stop_voice_chat` · `stop_zoned_clean`

### Delete (6)

`del_clean_record` · `del_clean_record_map_v2` · `del_map` · `del_mop_template_params` · `del_server_timer` · `del_timer`

### Other / map / service (50)

`add_mop_template_params` · `change_sound_volume` · `check_homesec_password` · `close_dnd_timer` · `close_valley_electricity_timer` · `dnld_install_sound`

`enable_homesec_voice` · `enable_log_upload` · `end_edit_map` · `find_me` · `load_multi_map` · `manual_bak_map`

`manual_segment_map` · `merge_segment` · `mop_mode` · `mop_template_id` · `name_multi_map` · `name_segment`

`play_audio` · `recover_map` · `recover_multi_map` · `reset_consumable` · `reset_homesec_password` · `reset_map`

`resolve_error` · `resume_segment_clean` · `resume_zoned_clean` · `retry_request` · `reunion_scenes` · `save_furnitures`

`save_map` · `send_ice_to_robot` · `send_sdp_to_robot` · `sort_mop_template_params` · `split_segment` · `switch_video_quality`

`switch_water_mark` · `test_sound_volume` · `upd_server_timer` · `upd_timer` · `update_dock` · `update_mop_template_params`

`upload_data_for_debug_mode` · `upload_photo` · `use_new_map` · `use_old_map` · `user_upload_log` · `matter.get_status`

`matter.dnld_key` · `matter.reset`

## 2. V1 commands explicitly wired into source traits (32)

- `app_get_init_status` — `roborock/cli.py:1172`, `roborock/devices/traits/v1/device_features.py:34`
- `app_start_wash` — `roborock/devices/traits/v1/wash_towel_mode.py:45`
- `app_stop_wash` — `roborock/devices/traits/v1/wash_towel_mode.py:49`
- `change_sound_volume` — `roborock/devices/traits/v1/volume.py:24`
- `close_dnd_timer` — `roborock/devices/traits/v1/do_not_disturb.py:26`, `roborock/devices/traits/v1/do_not_disturb.py:40`
- `close_valley_electricity_timer` — `roborock/devices/traits/v1/valley_electricity_timer.py:27`, `roborock/devices/traits/v1/valley_electricity_timer.py:42`
- `get_child_lock_status` — `roborock/devices/traits/v1/child_lock.py:11`
- `get_clean_record` — `roborock/devices/traits/v1/clean_summary.py:90`, `roborock/devices/traits/v1/clean_summary.py:96`
- `get_clean_summary` — `roborock/devices/traits/v1/clean_summary.py:71`
- `get_consumable` — `roborock/devices/traits/v1/consumeable.py:52`
- `get_dnd_timer` — `roborock/devices/traits/v1/do_not_disturb.py:11`
- `get_dust_collection_mode` — `roborock/devices/traits/v1/dust_collection_mode.py:12`
- `get_flow_led_status` — `roborock/devices/traits/v1/flow_led_status.py:11`
- `get_led_status` — `roborock/devices/traits/v1/led_status.py:28`
- `get_map_v1` — `roborock/devices/traits/v1/home.py:42`, `roborock/devices/traits/v1/map_content.py:86`
- `get_multi_maps_list` — `roborock/devices/traits/v1/maps.py:53`
- `get_network_info` — `roborock/devices/rpc/v1_channel.py:390`, `roborock/devices/traits/v1/network_info.py:33`
- `get_room_mapping` — `roborock/devices/traits/v1/rooms.py:84`
- `get_smart_wash_params` — `roborock/devices/traits/v1/smart_wash_params.py:12`
- `get_sound_volume` — `roborock/devices/traits/v1/volume.py:19`
- `get_status` — `roborock/devices/traits/v1/status.py:52`
- `get_valley_electricity_timer` — `roborock/devices/traits/v1/valley_electricity_timer.py:11`
- `get_wash_towel_mode` — `roborock/devices/traits/v1/wash_towel_mode.py:16`
- `load_multi_map` — `roborock/devices/traits/v1/maps.py:82`
- `reset_consumable` — `roborock/devices/traits/v1/consumeable.py:62`
- `set_child_lock_status` — `roborock/devices/traits/v1/child_lock.py:22`, `roborock/devices/traits/v1/child_lock.py:28`
- `set_dnd_timer` — `roborock/devices/traits/v1/do_not_disturb.py:21`, `roborock/devices/traits/v1/do_not_disturb.py:32`
- `set_flow_led_status` — `roborock/devices/traits/v1/flow_led_status.py:22`, `roborock/devices/traits/v1/flow_led_status.py:28`
- `set_led_status` — `roborock/devices/traits/v1/led_status.py:39`, `roborock/devices/traits/v1/led_status.py:45`
- `set_valley_electricity_timer` — `roborock/devices/traits/v1/valley_electricity_timer.py:22`, `roborock/devices/traits/v1/valley_electricity_timer.py:33`
- `set_wash_towel_mode` — `roborock/devices/traits/v1/wash_towel_mode.py:41`

## 3. B01/Q7 named method catalog

60 known Q7 method strings are declared. The user-facing action wrappers include `service.set_room_clean`, `service.start_recharge`, and `service.find_device`; details are in `roborock/devices/traits/b01/q7/__init__.py`.

`add_clean_failed.post` · `event.add_clean_failed.post` · `clean_finish.post` · `event.clean_finish.post` · `event.BuildMapFinish.post` · `event.map_change.post`

`event.work_appoint_clean_failed.post` · `startClean.post` · `service.add_order` · `service.add_sweep_clean` · `service.arrange_room` · `service.del_map`

`service.del_order` · `service.del_orders` · `service.delete_record_by_url` · `service.download_voice_type` · `service.erase_preference` · `service.find_device`

`service.get_room_order` · `service.get_voice_download` · `service.hello_wikka` · `service.rename_map` · `service.rename_room` · `service.rename_rooms`

`service.replace_map` · `service.reset_consumable` · `service.save_carpet` · `service.save_recommend_fb` · `service.save_sill` · `service.set_area_start`

`service.set_areas_start` · `service.set_cur_map` · `service.set_direction` · `service.set_global_sort` · `service.set_map_hide` · `service.set_multi_room_material`

`service.set_point_clean` · `service.set_preference` · `service.set_preference_type` · `service.set_quiet_time` · `service.set_room_clean` · `service.set_room_order`

`service.set_virtual_wall` · `service.set_zone_clean` · `service.set_zone_points` · `service.split_room` · `service.start_explore` · `service.start_point_clean`

`service.start_recharge` · `service.stop_recharge` · `service.upload_by_mapid` · `service.upload_record_by_url` · `prop.get` · `service.get_map_list`

`service.upload_by_maptype` · `prop.set` · `service.get_preference` · `service.get_record_list` · `service.get_order` · `prop.post`

## 4. B01/Q10 direct robot actions

- Start whole-home cleaning: DP `201`, parameter `1`
- Start room/segment cleaning: DP `201`, parameter `{"cmd": 2, "clean_paramters": [room IDs]}`
- Spot clean: DP `201`, parameter `5`
- Pause: DP `204`, parameter `0`
- Resume: DP `205`, parameter `0`
- Stop: DP `206`, parameter `0`
- Return to dock/charge: DP `202`, parameter `5`
- Empty dustbin: DP `203`, parameter `2`
- Set cleaning mode: DP `137` (enum-mapped)
- Set fan level: DP `123` (enum-mapped)
8 changes: 5 additions & 3 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ and with the message broker (MQTT) that **your server** designates. This is used
The developer does **not** receive any of this traffic. The app contacts no
developer-operated or analytics server.

The only other outbound connections are **links you tap yourself**: an optional
link to the open-source project page (GitHub) and an optional donation link.
These open in your browser and are only visited if you choose to tap them.
The only other outbound connections are **links you tap yourself**: optional links
to the open-source project page (GitHub) and this privacy policy. These open in
your browser and are only visited if you choose to tap them.

## 4. Camera / live view

Expand All @@ -70,6 +70,8 @@ The app requests only the permissions it needs to function:
- **Internet / network state** — to reach the server you configure.
- **Wi-Fi state / nearby Wi-Fi devices** — to pair a new vacuum with your Wi-Fi
network during setup.
- **Local network (iOS)** — to reach your server and to exchange the setup
handshake with the robot's own access point while pairing it.
- **Location (Android 12 and below only)** — required by older versions of
Android to scan for Wi-Fi networks during vacuum pairing. The app does not use
or collect your location, and this permission is not requested on Android 13+.
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ LocalRock is a [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.
</a>
</td>
<td valign="middle">
<a href="https://apps.apple.com/app/localrock/id6788538720">
<a href="https://apps.apple.com/dk/app/localrock/id6788538720">
<img alt="Download on the App Store" height="40"
src="https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg">
</a>
Expand Down Expand Up @@ -221,11 +221,13 @@ tests/ # protocol/contract fixtures & tests
| MQTT over TLS | Yes (KMQTT) | Yes (CocoaMQTT bridge) |
| Camera live view (WebRTC) | Implemented, **untested** | Implemented (WebRTC.framework bridge), **untested** |
| RSA (onboarding crypto) | Yes | Yes |
| Phone Wi‑Fi pairing (Add vacuum) | Yes | Not yet implemented |
| Phone Wi‑Fi pairing (Add vacuum) | Yes | Yes |

**Camera live view note:** the WebRTC live feed is implemented from the protocol reference but has **not been tested or confirmed to work** on real hardware. Treat it as experimental.

**iOS note:** Kotlin/Native iOS targets must be built on macOS. The iOS MQTT and WebRTC integrations rely on Swift Package Manager dependencies ([CocoaMQTT](https://github.com/emqx/CocoaMQTT), [WebRTC](https://github.com/stasel/WebRTC)) that must be added to the `iosApp` Xcode target, and the Swift bridge files (`CocoaMqttTransport.swift`, `WebRtcPeer.swift`) added to the target. Pass `nil` for the WebRTC factory to ship without live view. iOS uses a custom bundle identifier that needs signing/provisioning selected in Xcode.
**iOS note:** Kotlin/Native iOS targets must be built on macOS. The iOS MQTT and WebRTC integrations rely on Swift Package Manager dependencies ([CocoaMQTT](https://github.com/emqx/CocoaMQTT), [WebRTC](https://github.com/stasel/WebRTC)) that must be added to the `iosApp` Xcode target, and the Swift bridge files (`CocoaMqttTransport.swift`, `WebRtcPeer.swift`, `VacuumPairing.swift`) added to the target. Pass `nil` for the WebRTC factory to ship without live view, or `nil` for the pairing factory to hide the "add vacuum" flow. iOS uses a custom bundle identifier that needs signing/provisioning selected in Xcode.

**iOS pairing entitlement:** joining the robot's access point uses `NEHotspotConfiguration`, which needs the **Hotspot Configuration** capability. It is declared in `iosApp/iosApp.entitlements` and wired into the target, but it only signs once the capability is enabled for the App ID in the Apple Developer portal (Certificates, Identifiers & Profiles → your App ID → Hotspot Configuration). Without it the app fails to sign; with it, the UDP handshake also triggers the local-network prompt backed by `NSLocalNetworkUsageDescription`.

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,5 @@ actual fun createVacuumPairingTransport(): VacuumPairingTransport {
?: throw VacuumPairingException("Android context not initialized; call initVacLocalAndroidContext first")
return VacuumPairingTransport(ctx)
}

actual val vacuumPairingSupported: Boolean = true
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ object DemoData {
private val products: List<Product> = listOf(
Product(
id = "demo-prod-s8",
name = "S8 Pro Ultra",
model = "roborock.vacuum.a70",
name = "Robot Vacuum",
model = "Vacuum",
category = "robot.vacuum.cleaner",
),
Product(
id = "demo-prod-q7",
name = "Q7 Max+",
model = "roborock.vacuum.a38",
name = "Robot Vacuum",
model = "Vacuum",
category = "robot.vacuum.cleaner",
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@ class VacuumPairingException(message: String, cause: Throwable? = null) : Runtim

expect fun createVacuumPairingTransport(): VacuumPairingTransport

/** False when the platform cannot pair over Wi-Fi, so the UI can hide the flow instead of failing. */
expect val vacuumPairingSupported: Boolean

const val VACUUM_PAIRING_HOST: String = "192.168.8.1"
const val VACUUM_PAIRING_PORT: Int = 55559
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import androidx.compose.ui.unit.dp
import com.kodraliu.localrock.resources.Res
import com.kodraliu.localrock.resources.app_icon
import com.kodraliu.localrock.shared.model.Device
import com.kodraliu.localrock.shared.onboarding.vacuumPairingSupported
import com.kodraliu.localrock.ui.AppColors
import org.jetbrains.compose.resources.painterResource

Expand Down Expand Up @@ -91,8 +92,10 @@ fun DeviceListScreen(
)
},
floatingActionButton = {
FloatingActionButton(onClick = onAddVacuum) {
Icon(Icons.Default.Add, contentDescription = "Add vacuum")
if (vacuumPairingSupported) {
FloatingActionButton(onClick = onAddVacuum) {
Icon(Icons.Default.Add, contentDescription = "Add vacuum")
}
}
},
) { padding ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fun SplashScreen(onTimeout: () -> Unit) {
color = MaterialTheme.colorScheme.primary,
)
Text(
"Local control for Roborock",
"Local control for your robot vacuum",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ fun WelcomeScreen() {
fontWeight = FontWeight.Bold,
)
Text(
"Control your Roborock vacuum over your own network — please read this first.",
"Control your robot vacuum over your own network — please read this first.",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Expand All @@ -60,7 +60,7 @@ fun WelcomeScreen() {
"2",
"Sign in with your server credentials",
"You'll sign in with the email and login code you configured during server setup — " +
"not a new Roborock account.",
"not a new cloud account.",
)
Point(
"3",
Expand Down
Loading
Loading