Skip to content

Commit 41d5433

Browse files
Lash-Lallenporter
andauthored
feat: add the ability to update supported_features via cli (#428)
* feat: add the ability to update supported_features via cli * chore: update doc * chore: fix lint errors from merge * fix: change to store info in a yaml file --------- Co-authored-by: Allen Porter <allen.porter@gmail.com>
1 parent aec476c commit 41d5433

File tree

3 files changed

+361
-2
lines changed

3 files changed

+361
-2
lines changed

SUPPORTED_FEATURES.md

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
| Feature | roborock.vacuum.a15 | roborock.vacuum.a87 |
2+
|---|---|---|
3+
| Product Nickname | TANOSS | PEARLPLUS |
4+
| Protocol Version | 1.0 | 1.0 |
5+
| New Feature Info | 636084721975295 | 4499197267967999 |
6+
| New Feature Info Str | 0000000000002000 | 508A977F7EFEFFFF |
7+
| `111` | X | X |
8+
| `112` | X | X |
9+
| `113` | X | X |
10+
| `114` | X | X |
11+
| `115` | X | X |
12+
| `116` | X | X |
13+
| `117` | X | X |
14+
| `118` | X | X |
15+
| `119` | X | X |
16+
| `120` | X | X |
17+
| `121` | | X |
18+
| `122` | X | X |
19+
| `123` | X | X |
20+
| `124` | X | X |
21+
| `125` | X | X |
22+
| `is_activate_video_charging_and_standby_supported` | | |
23+
| `is_analysis_supported` | X | X |
24+
| `is_any_state_transit_goto_supported` | X | X |
25+
| `is_auto_collection_2_supported` | | |
26+
| `is_auto_delivery_field_in_global_status_supported` | | X |
27+
| `is_auto_tear_down_mop_supported` | | |
28+
| `is_avoid_collision_mode_supported` | | X |
29+
| `is_avoid_collision_supported` | | X |
30+
| `is_back_charge_auto_wash_supported` | | X |
31+
| `is_careful_slow_mop_supported` | X | X |
32+
| `is_carpet_custom_clean_supported` | | X |
33+
| `is_carpet_deep_clean_supported` | | X |
34+
| `is_carpet_long_haired_supported` | | |
35+
| `is_carpet_pressure_use_origin_paras_supported` | | |
36+
| `is_carpet_show_on_map` | | X |
37+
| `is_carpet_supported` | X | X |
38+
| `is_ces2022_supported` | | |
39+
| `is_clean_count_setting_supported` | | X |
40+
| `is_clean_direct_status_supported` | | |
41+
| `is_clean_history_time_line_supported` | | |
42+
| `is_clean_route_deep_slow_plus_supported` | | |
43+
| `is_clean_route_fast_mode_supported` | | X |
44+
| `is_clean_route_setting_supported` | | |
45+
| `is_collect_dust_mode_supported` | X | X |
46+
| `is_corner_clean_mode_supported` | | |
47+
| `is_corner_mop_stretch_supported` | | X |
48+
| `is_current_map_restore_enabled` | X | X |
49+
| `is_custom_clean_mode_count_supported` | | X |
50+
| `is_custom_mode_supported` | X | X |
51+
| `is_custom_water_box_distance_supported` | | X |
52+
| `is_dirty_object_detect_supported` | | |
53+
| `is_dirty_replenish_clean_supported` | | X |
54+
| `is_dry_interval_timer_supported` | | |
55+
| `is_dss_believable` | | X |
56+
| `is_dust_collection_setting_supported` | X | X |
57+
| `is_dynamically_add_clean_zones_supported` | | X |
58+
| `is_dynamically_skip_clean_zone_supported` | | X |
59+
| `is_egg_dance_mode_supported` | | |
60+
| `is_egg_mode_supported_from_new_features` | | |
61+
| `is_exact_custom_mode_supported` | | X |
62+
| `is_exhibition_function_supported` | | |
63+
| `is_floor_dir_clean_any_time_supported` | | X |
64+
| `is_flow_led_setting_supported` | X | |
65+
| `is_fw_filter_obstacle_supported` | X | X |
66+
| `is_gap_deep_clean_supported` | | |
67+
| `is_goto_pure_clean_path_supported` | | X |
68+
| `is_hot_wash_towel_supported` | | X |
69+
| `is_identify_room_supported` | | |
70+
| `is_ignore_unknown_map_object_supported` | X | X |
71+
| `is_lds_lifting_supported` | | |
72+
| `is_led_status_switch_supported` | X | X |
73+
| `is_left_water_drain_supported` | | X |
74+
| `is_main_brush_up_down_supported_from_str` | | X |
75+
| `is_map_beautify_internal_debug_supported` | X | X |
76+
| `is_map_carpet_add_support` | | X |
77+
| `is_map_eraser_supported` | | |
78+
| `is_matter_supported` | | |
79+
| `is_max_plus_mode_supported` | | |
80+
| `is_max_zone_opened_supported` | | |
81+
| `is_midway_back_to_dock_supported` | | |
82+
| `is_min_battery_15_to_clean_task_supported` | | X |
83+
| `is_mop_forbidden_supported` | | |
84+
| `is_mop_path_supported` | X | X |
85+
| `is_mop_shake_water_max_supported` | | |
86+
| `is_multi_floor_supported` | X | X |
87+
| `is_multi_map_segment_timer_supported` | X | X |
88+
| `is_new_ai_recognition_supported` | | |
89+
| `is_new_data_for_clean_history` | X | X |
90+
| `is_new_data_for_clean_history_detail` | X | X |
91+
| `is_new_endpoint_supported` | | X |
92+
| `is_new_remote_view_supported` | | |
93+
| `is_no_need_carpet_press_set_supported` | | |
94+
| `is_none_pure_clean_mop_with_max_plus` | | |
95+
| `is_object_detect_check_supported` | | |
96+
| `is_offline_map_supported` | | X |
97+
| `is_optimize_battery_supported` | | |
98+
| `is_order_clean_supported` | X | X |
99+
| `is_pet_snapshot_supported` | | |
100+
| `is_pet_supplies_deep_clean_supported` | | |
101+
| `is_pumping_water_supported` | | |
102+
| `is_pure_clean_mop_supported` | | |
103+
| `is_re_segment_supported` | X | X |
104+
| `is_record_allowed` | X | X |
105+
| `is_remote_supported` | X | X |
106+
| `is_right_brush_stretch_supported` | | |
107+
| `is_room_name_supported` | X | X |
108+
| `is_rpc_retry_supported` | | X |
109+
| `is_rubber_brush_carpet_supported` | | |
110+
| `is_set_child_supported` | X | X |
111+
| `is_setting_carpet_first_supported` | | X |
112+
| `is_shake_mop_set_supported` | X | X |
113+
| `is_show_clean_finish_reason_supported` | X | X |
114+
| `is_show_general_obstacle_supported` | | |
115+
| `is_show_obstacle_photo_supported` | | |
116+
| `is_small_side_mop_supported` | | |
117+
| `is_smart_clean_mode_set_supported` | | X |
118+
| `is_soft_clean_mode_supported` | | |
119+
| `is_super_deep_wash_supported` | | X |
120+
| `is_support_backup_map` | X | X |
121+
| `is_support_clean_estimate` | | X |
122+
| `is_support_cliff_zone` | | X |
123+
| `is_support_custom_carpet` | | |
124+
| `is_support_custom_dnd` | | X |
125+
| `is_support_custom_door_sill` | | X |
126+
| `is_support_custom_mode_in_cleaning` | | X |
127+
| `is_support_fetch_timer_summary` | X | X |
128+
| `is_support_floor_direction` | | X |
129+
| `is_support_floor_edit` | | X |
130+
| `is_support_furniture` | | X |
131+
| `is_support_incremental_map` | | X |
132+
| `is_support_main_brush_up_down_supported` | | |
133+
| `is_support_mop_back_pwm_set` | | |
134+
| `is_support_quick_map_builder` | X | X |
135+
| `is_support_remote_control_in_call` | | X |
136+
| `is_support_room_tag` | | X |
137+
| `is_support_set_switch_map_mode` | | X |
138+
| `is_support_set_volume_in_call` | | X |
139+
| `is_support_side_brush_up_down_supported` | | |
140+
| `is_support_smart_door_sill` | | X |
141+
| `is_support_smart_global_clean_with_custom_mode` | | X |
142+
| `is_support_smart_scene` | | X |
143+
| `is_support_stuck_zone` | | X |
144+
| `is_support_voice_control_debug` | | |
145+
| `is_support_water_mode` | | |
146+
| `is_supported_download_test_voice` | | X |
147+
| `is_supported_drying` | | X |
148+
| `is_supported_valley_electricity` | | X |
149+
| `is_two_key_real_time_video_supported` | | X |
150+
| `is_two_key_rtv_in_charging_supported` | | X |
151+
| `is_unsave_map_reason_supported` | X | X |
152+
| `is_uvc_sterilize_supported` | | |
153+
| `is_video_monitor_supported` | X | X |
154+
| `is_video_patrol_supported` | | |
155+
| `is_video_setting_supported` | X | X |
156+
| `is_voice_control_led_supported` | | |
157+
| `is_voice_control_supported` | | X |
158+
| `is_wash_then_charge_cmd_supported` | | X |
159+
| `is_water_leak_check_supported` | | X |
160+
| `is_water_up_down_drain_supported` | | X |
161+
| `is_wifi_manage_supported` | | X |
162+
| `is_workday_holiday_supported` | | |

device_info.yaml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
roborock.vacuum.a15:
2+
Protocol Version: '1.0'
3+
Product Nickname: TANOSS
4+
New Feature Info: 636084721975295
5+
New Feature Info Str: '0000000000002000'
6+
Feature Info:
7+
- 111
8+
- 112
9+
- 113
10+
- 114
11+
- 115
12+
- 116
13+
- 117
14+
- 118
15+
- 119
16+
- 120
17+
- 122
18+
- 123
19+
- 124
20+
- 125
21+
roborock.vacuum.a87:
22+
Protocol Version: '1.0'
23+
Product Nickname: PEARLPLUS
24+
New Feature Info: 4499197267967999
25+
New Feature Info Str: 508A977F7EFEFFFF
26+
Feature Info:
27+
- 111
28+
- 112
29+
- 113
30+
- 114
31+
- 115
32+
- 116
33+
- 117
34+
- 118
35+
- 119
36+
- 120
37+
- 121
38+
- 122
39+
- 123
40+
- 124
41+
- 125

roborock/cli.py

Lines changed: 158 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import asyncio
22
import json
33
import logging
4-
from dataclasses import dataclass
4+
from dataclasses import asdict, dataclass
55
from pathlib import Path
66
from typing import Any
77

88
import click
9+
import yaml
910
from pyshark import FileCapture # type: ignore
1011
from pyshark.capture.live_capture import LiveCapture, UnknownInterfaceException # type: ignore
1112
from pyshark.packet.packet import Packet # type: ignore
1213

13-
from roborock import RoborockException
14+
from roborock import SHORT_MODEL_TO_ENUM, DeviceFeatures, RoborockCommand, RoborockException
1415
from roborock.containers import DeviceData, HomeData, HomeDataProduct, LoginData, NetworkInfo, RoborockBase, UserData
1516
from roborock.devices.cache import Cache, CacheData
1617
from roborock.devices.device_manager import create_device_manager, create_home_data_api
@@ -325,6 +326,159 @@ def on_package(packet: Packet):
325326
)
326327

327328

329+
@click.command()
330+
@click.pass_context
331+
@run_sync()
332+
async def get_device_info(ctx: click.Context):
333+
"""
334+
Connects to devices and prints their feature information in YAML format.
335+
"""
336+
click.echo("Discovering devices...")
337+
context: RoborockContext = await _load_and_discover(ctx)
338+
cache_data = context.cache_data()
339+
340+
home_data = cache_data.home_data
341+
342+
all_devices = home_data.devices + home_data.received_devices
343+
if not all_devices:
344+
click.echo("No devices found.")
345+
return
346+
347+
click.echo(f"Found {len(all_devices)} devices. Fetching data...")
348+
349+
all_products_data = {}
350+
351+
for device in all_devices:
352+
click.echo(f" - Processing {device.name} ({device.duid})")
353+
product_info = home_data.product_map[device.product_id]
354+
device_data = DeviceData(device, product_info.model)
355+
mqtt_client = RoborockMqttClientV1(cache_data.user_data, device_data)
356+
357+
try:
358+
init_status_result = await mqtt_client.send_command(
359+
RoborockCommand.APP_GET_INIT_STATUS,
360+
)
361+
product_nickname = SHORT_MODEL_TO_ENUM.get(product_info.model.split(".")[-1]).name
362+
current_product_data = {
363+
"Protocol Version": device.pv,
364+
"Product Nickname": product_nickname,
365+
"New Feature Info": init_status_result.get("new_feature_info"),
366+
"New Feature Info Str": init_status_result.get("new_feature_info_str"),
367+
"Feature Info": init_status_result.get("feature_info"),
368+
}
369+
370+
all_products_data[product_info.model] = current_product_data
371+
372+
except Exception as e:
373+
click.echo(f" - Error processing device {device.name}: {e}", err=True)
374+
finally:
375+
await mqtt_client.async_release()
376+
377+
if all_products_data:
378+
click.echo("\n--- Device Information (copy to your YAML file) ---\n")
379+
# Use yaml.dump to print in a clean, copy-paste friendly format
380+
click.echo(yaml.dump(all_products_data, sort_keys=False))
381+
382+
383+
@click.command()
384+
@click.option("--data-file", default="../device_info.yaml", help="Path to the YAML file with device feature data.")
385+
@click.option("--output-file", default="../SUPPORTED_FEATURES.md", help="Path to the output markdown file.")
386+
def update_docs(data_file: str, output_file: str):
387+
"""
388+
Generates a markdown file by processing raw feature data from a YAML file.
389+
"""
390+
data_path = Path(data_file)
391+
output_path = Path(output_file)
392+
393+
if not data_path.exists():
394+
click.echo(f"Error: Data file not found at '{data_path}'", err=True)
395+
return
396+
397+
click.echo(f"Loading data from {data_path}...")
398+
with open(data_path, encoding="utf-8") as f:
399+
product_data_from_yaml = yaml.safe_load(f)
400+
401+
if not product_data_from_yaml:
402+
click.echo("No data found in YAML file. Exiting.", err=True)
403+
return
404+
405+
product_features_map = {}
406+
all_feature_names = set()
407+
408+
# Process the raw data from YAML to build the feature map
409+
for model, data in product_data_from_yaml.items():
410+
# Reconstruct the DeviceFeatures object from the raw data in the YAML file
411+
device_features = DeviceFeatures.from_feature_flags(
412+
new_feature_info=data.get("New Feature Info"),
413+
new_feature_info_str=data.get("New Feature Info Str"),
414+
feature_info=data.get("Feature Info"),
415+
product_nickname=data.get("Product Nickname"),
416+
)
417+
features_dict = asdict(device_features)
418+
419+
# This dictionary will hold the final data for the markdown table row
420+
current_product_data = {
421+
"Product Nickname": data.get("Product Nickname", ""),
422+
"Protocol Version": data.get("Protocol Version", ""),
423+
"New Feature Info": data.get("New Feature Info", ""),
424+
"New Feature Info Str": data.get("New Feature Info Str", ""),
425+
}
426+
427+
# Populate features from the calculated DeviceFeatures object
428+
for feature, is_supported in features_dict.items():
429+
all_feature_names.add(feature)
430+
if is_supported:
431+
current_product_data[feature] = "X"
432+
433+
supported_codes = data.get("Feature Info", [])
434+
if isinstance(supported_codes, list):
435+
for code in supported_codes:
436+
feature_name = str(code)
437+
all_feature_names.add(feature_name)
438+
current_product_data[feature_name] = "X"
439+
440+
product_features_map[model] = current_product_data
441+
442+
# --- Helper function to write the markdown table ---
443+
def write_markdown_table(product_features: dict[str, dict[str, any]], all_features: set[str]):
444+
"""Writes the data into a markdown table (products as columns)."""
445+
sorted_products = sorted(product_features.keys())
446+
special_rows = [
447+
"Product Nickname",
448+
"Protocol Version",
449+
"New Feature Info",
450+
"New Feature Info Str",
451+
]
452+
# Regular features are the remaining keys, sorted alphabetically
453+
# We filter out the special rows to avoid duplicating them.
454+
sorted_features = sorted(list(all_features - set(special_rows)))
455+
456+
header = ["Feature"] + sorted_products
457+
458+
click.echo(f"Writing documentation to {output_path}...")
459+
with open(output_path, "w", encoding="utf-8") as f:
460+
f.write("| " + " | ".join(header) + " |\n")
461+
f.write("|" + "---|" * len(header) + "\n")
462+
463+
# Write the special metadata rows first
464+
for row_name in special_rows:
465+
row_values = [str(product_features[p].get(row_name, "")) for p in sorted_products]
466+
f.write("| " + " | ".join([row_name] + row_values) + " |\n")
467+
468+
# Write the feature rows
469+
for feature in sorted_features:
470+
# Use backticks for feature names that are just numbers (from the list)
471+
display_feature = f"`{feature}`"
472+
feature_row = [display_feature]
473+
for product in sorted_products:
474+
# Use .get() to place an 'X' or an empty string
475+
feature_row.append(product_features[product].get(feature, ""))
476+
f.write("| " + " | ".join(feature_row) + " |\n")
477+
478+
write_markdown_table(product_features_map, all_feature_names)
479+
click.echo("Done.")
480+
481+
328482
cli.add_command(login)
329483
cli.add_command(discover)
330484
cli.add_command(list_devices)
@@ -334,6 +488,8 @@ def on_package(packet: Packet):
334488
cli.add_command(command)
335489
cli.add_command(parser)
336490
cli.add_command(session)
491+
cli.add_command(get_device_info)
492+
cli.add_command(update_docs)
337493

338494

339495
def main():

0 commit comments

Comments
 (0)