python-roborock 4.5.0__tar.gz → 4.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. {python_roborock-4.5.0 → python_roborock-4.7.0}/PKG-INFO +1 -1
  2. {python_roborock-4.5.0 → python_roborock-4.7.0}/pyproject.toml +2 -2
  3. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/cli.py +208 -56
  4. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/zeo/zeo_code_mappings.py +2 -0
  5. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/device_features.py +14 -1
  6. {python_roborock-4.5.0 → python_roborock-4.7.0}/.gitignore +0 -0
  7. {python_roborock-4.5.0 → python_roborock-4.7.0}/LICENSE +0 -0
  8. {python_roborock-4.5.0 → python_roborock-4.7.0}/README.md +0 -0
  9. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/__init__.py +0 -0
  10. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/broadcast_protocol.py +0 -0
  11. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/callbacks.py +0 -0
  12. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/const.py +0 -0
  13. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/__init__.py +0 -0
  14. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/b01_q10/__init__.py +0 -0
  15. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  16. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  17. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/b01_q7/__init__.py +0 -0
  18. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  19. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  20. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/code_mappings.py +0 -0
  21. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/containers.py +0 -0
  22. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/dyad/__init__.py +0 -0
  23. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  24. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/dyad/dyad_containers.py +0 -0
  25. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/v1/__init__.py +0 -0
  26. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  27. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/v1/v1_code_mappings.py +0 -0
  28. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/v1/v1_containers.py +0 -0
  29. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/zeo/__init__.py +0 -0
  30. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/data/zeo/zeo_containers.py +0 -0
  31. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/README.md +0 -0
  32. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/__init__.py +0 -0
  33. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/cache.py +0 -0
  34. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/device.py +0 -0
  35. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/device_manager.py +0 -0
  36. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/file_cache.py +0 -0
  37. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/rpc/__init__.py +0 -0
  38. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/rpc/a01_channel.py +0 -0
  39. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
  40. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
  41. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/rpc/v1_channel.py +0 -0
  42. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/__init__.py +0 -0
  43. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/a01/__init__.py +0 -0
  44. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/b01/__init__.py +0 -0
  45. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
  46. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/b01/q10/command.py +0 -0
  47. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
  48. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/traits_mixin.py +0 -0
  49. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/__init__.py +0 -0
  50. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  51. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  52. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/command.py +0 -0
  53. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/common.py +0 -0
  54. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  55. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/device_features.py +0 -0
  56. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  57. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  58. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  59. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/home.py +0 -0
  60. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/led_status.py +0 -0
  61. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/map_content.py +0 -0
  62. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/maps.py +0 -0
  63. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/network_info.py +0 -0
  64. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/rooms.py +0 -0
  65. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/routines.py +0 -0
  66. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  67. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/status.py +0 -0
  68. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  69. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/volume.py +0 -0
  70. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  71. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/transport/__init__.py +0 -0
  72. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/transport/channel.py +0 -0
  73. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/transport/local_channel.py +0 -0
  74. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/devices/transport/mqtt_channel.py +0 -0
  75. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/diagnostics.py +0 -0
  76. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/exceptions.py +0 -0
  77. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/map/__init__.py +0 -0
  78. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/map/map_parser.py +0 -0
  79. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/mqtt/__init__.py +0 -0
  80. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/mqtt/health_manager.py +0 -0
  81. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/mqtt/roborock_session.py +0 -0
  82. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/mqtt/session.py +0 -0
  83. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/protocol.py +0 -0
  84. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/protocols/__init__.py +0 -0
  85. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/protocols/a01_protocol.py +0 -0
  86. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/protocols/b01_q10_protocol.py +0 -0
  87. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/protocols/b01_q7_protocol.py +0 -0
  88. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/protocols/v1_protocol.py +0 -0
  89. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/py.typed +0 -0
  90. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/roborock_message.py +0 -0
  91. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/roborock_typing.py +0 -0
  92. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/util.py +0 -0
  93. {python_roborock-4.5.0 → python_roborock-4.7.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 4.5.0
3
+ Version: 4.7.0
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Project-URL: Repository, https://github.com/humbertogontijo/python-roborock
6
6
  Project-URL: Documentation, https://python-roborock.readthedocs.io/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-roborock"
3
- version = "4.5.0"
3
+ version = "4.7.0"
4
4
  description = "A package to control Roborock vacuums."
5
5
  authors = [{ name = "humbertogontijo", email = "humbertogontijo@users.noreply.github.com" }, {name="Lash-L"}, {name="allenporter"}]
6
6
  requires-python = ">=3.11, <4"
@@ -44,7 +44,7 @@ dev = [
44
44
  "pytest",
45
45
  "pre-commit>=3.5,<5.0",
46
46
  "mypy",
47
- "ruff==0.14.10",
47
+ "ruff==0.14.11",
48
48
  "codespell",
49
49
  "pyshark>=0.6,<0.7",
50
50
  "aioresponses>=0.7.7,<0.8",
@@ -44,6 +44,7 @@ from pyshark.packet.packet import Packet # type: ignore
44
44
  from roborock import RoborockCommand
45
45
  from roborock.data import RoborockBase, UserData
46
46
  from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
47
+ from roborock.data.code_mappings import SHORT_MODEL_TO_ENUM
47
48
  from roborock.device_features import DeviceFeatures
48
49
  from roborock.devices.cache import Cache, CacheData
49
50
  from roborock.devices.device import RoborockDevice
@@ -299,6 +300,12 @@ def cli(ctx, debug: int):
299
300
 
300
301
  @click.command()
301
302
  @click.option("--email", required=True)
303
+ @click.option(
304
+ "--reauth",
305
+ is_flag=True,
306
+ default=False,
307
+ help="Re-authenticate even if cached credentials exist.",
308
+ )
302
309
  @click.option(
303
310
  "--password",
304
311
  required=False,
@@ -306,15 +313,16 @@ def cli(ctx, debug: int):
306
313
  )
307
314
  @click.pass_context
308
315
  @async_command
309
- async def login(ctx, email, password):
316
+ async def login(ctx, email, password, reauth):
310
317
  """Login to Roborock account."""
311
318
  context: RoborockContext = ctx.obj
312
- try:
313
- context.validate()
314
- _LOGGER.info("Already logged in")
315
- return
316
- except RoborockException:
317
- pass
319
+ if not reauth:
320
+ try:
321
+ context.validate()
322
+ _LOGGER.info("Already logged in")
323
+ return
324
+ except RoborockException:
325
+ pass
318
326
  client = RoborockApiClient(email)
319
327
  if password is not None:
320
328
  user_data = await client.pass_login(password)
@@ -834,59 +842,203 @@ async def parser(_, local_key, device_ip, file):
834
842
  )
835
843
 
836
844
 
845
+ def _parse_diagnostic_file(diagnostic_path: Path) -> dict[str, dict[str, Any]]:
846
+ """Parse device info from a Home Assistant diagnostic file.
847
+
848
+ Args:
849
+ diagnostic_path: Path to the diagnostic JSON file.
850
+
851
+ Returns:
852
+ A dictionary mapping model names to device info dictionaries.
853
+ """
854
+ with open(diagnostic_path, encoding="utf-8") as f:
855
+ diagnostic_data = json.load(f)
856
+
857
+ all_products_data: dict[str, dict[str, Any]] = {}
858
+
859
+ # Navigate to coordinators in the diagnostic data
860
+ coordinators = diagnostic_data.get("data", {}).get("coordinators", {})
861
+ if not coordinators:
862
+ return all_products_data
863
+
864
+ for coordinator_data in coordinators.values():
865
+ device_data = coordinator_data.get("device", {})
866
+ product_data = coordinator_data.get("product", {})
867
+
868
+ model = product_data.get("model")
869
+ if not model or model in all_products_data:
870
+ continue
871
+ # Derive product nickname from model
872
+ short_model = model.split(".")[-1]
873
+ product_nickname = SHORT_MODEL_TO_ENUM.get(short_model)
874
+
875
+ current_product_data: dict[str, Any] = {
876
+ "protocol_version": device_data.get("pv"),
877
+ "product_nickname": product_nickname.name if product_nickname else "Unknown",
878
+ }
879
+
880
+ # Get feature info from the device_features trait (preferred location)
881
+ traits_data = coordinator_data.get("traits", {})
882
+ device_features = traits_data.get("device_features", {})
883
+
884
+ # newFeatureInfo is the integer
885
+ new_feature_info = device_features.get("newFeatureInfo")
886
+ if new_feature_info is not None:
887
+ current_product_data["new_feature_info"] = new_feature_info
888
+
889
+ # newFeatureInfoStr is the hex string
890
+ new_feature_info_str = device_features.get("newFeatureInfoStr")
891
+ if new_feature_info_str:
892
+ current_product_data["new_feature_info_str"] = new_feature_info_str
893
+
894
+ # featureInfo is the list of feature codes
895
+ feature_info = device_features.get("featureInfo")
896
+ if feature_info:
897
+ current_product_data["feature_info"] = feature_info
898
+
899
+ # Build product dict from diagnostic product data
900
+ if product_data:
901
+ # Convert to the format expected by device_info.yaml
902
+ product_dict: dict[str, Any] = {}
903
+ for key in ["id", "name", "model", "category", "capability", "schema"]:
904
+ if key in product_data:
905
+ product_dict[key] = product_data[key]
906
+ if product_dict:
907
+ current_product_data["product"] = product_dict
908
+
909
+ all_products_data[model] = current_product_data
910
+
911
+ return all_products_data
912
+
913
+
837
914
  @click.command()
915
+ @click.option(
916
+ "--record",
917
+ is_flag=True,
918
+ default=False,
919
+ help="Save new device info entries to the YAML file.",
920
+ )
921
+ @click.option(
922
+ "--device-info-file",
923
+ default="device_info.yaml",
924
+ help="Path to the YAML file with device and product data.",
925
+ )
926
+ @click.option(
927
+ "--diagnostic-file",
928
+ default=None,
929
+ help="Path to a Home Assistant diagnostic JSON file to parse instead of connecting to devices.",
930
+ )
838
931
  @click.pass_context
839
932
  @async_command
840
- async def get_device_info(ctx: click.Context):
933
+ async def get_device_info(ctx: click.Context, record: bool, device_info_file: str, diagnostic_file: str | None):
841
934
  """
842
935
  Connects to devices and prints their feature information in YAML format.
936
+
937
+ Can also parse device info from a Home Assistant diagnostic file using --diagnostic-file.
843
938
  """
844
- click.echo("Discovering devices...")
845
939
  context: RoborockContext = ctx.obj
846
- device_connection_manager = await context.get_device_manager()
847
- device_manager = await device_connection_manager.ensure_device_manager()
848
- devices = await device_manager.get_devices()
849
- if not devices:
850
- click.echo("No devices found.")
851
- return
940
+ device_info_path = Path(device_info_file)
941
+ existing_device_info: dict[str, Any] = {}
942
+
943
+ # Load existing device info if recording
944
+ if record:
945
+ click.echo(f"Using device info file: {device_info_path.resolve()}")
946
+ if device_info_path.exists():
947
+ with open(device_info_path, encoding="utf-8") as f:
948
+ data = yaml.safe_load(f)
949
+ if isinstance(data, dict):
950
+ existing_device_info = data
951
+
952
+ # Parse from diagnostic file if provided
953
+ if diagnostic_file:
954
+ diagnostic_path = Path(diagnostic_file)
955
+ if not diagnostic_path.exists():
956
+ click.echo(f"Diagnostic file not found: {diagnostic_path}", err=True)
957
+ return
958
+
959
+ click.echo(f"Parsing diagnostic file: {diagnostic_path.resolve()}")
960
+ all_products_data = _parse_diagnostic_file(diagnostic_path)
961
+
962
+ if not all_products_data:
963
+ click.echo("No device data found in diagnostic file.")
964
+ return
965
+
966
+ click.echo(f"Found {len(all_products_data)} device(s) in diagnostic file.")
852
967
 
853
- click.echo(f"Found {len(devices)} devices. Fetching data...")
968
+ else:
969
+ click.echo("Discovering devices...")
854
970
 
855
- all_products_data = {}
971
+ if record:
972
+ connection_cache = await context.get_devices()
973
+ home_data = connection_cache.cache_data.home_data if connection_cache.cache_data else None
974
+ if home_data is None:
975
+ click.echo("Home data not available.", err=True)
976
+ return
856
977
 
857
- for device in devices:
858
- click.echo(f" - Processing {device.name} ({device.duid})")
978
+ device_connection_manager = await context.get_device_manager()
979
+ device_manager = await device_connection_manager.ensure_device_manager()
980
+ devices = await device_manager.get_devices()
981
+ if not devices:
982
+ click.echo("No devices found.")
983
+ return
859
984
 
860
- if device.product.model in all_products_data:
861
- click.echo(f" - Skipping duplicate model {device.product.model}")
862
- continue
985
+ click.echo(f"Found {len(devices)} devices. Fetching data...")
863
986
 
864
- current_product_data = {
865
- "Protocol Version": device.device_info.pv,
866
- "Product Nickname": device.product.product_nickname.name,
867
- }
868
- if device.v1_properties is not None:
869
- try:
870
- result: list[dict[str, Any]] = await device.v1_properties.command.send(
871
- RoborockCommand.APP_GET_INIT_STATUS
872
- )
873
- except Exception as e:
874
- click.echo(f" - Error processing device {device.name}: {e}", err=True)
987
+ all_products_data = {}
988
+
989
+ for device in devices:
990
+ click.echo(f" - Processing {device.name} ({device.duid})")
991
+
992
+ model = device.product.model
993
+ if model in all_products_data:
994
+ click.echo(f" - Skipping duplicate model {model}")
875
995
  continue
876
- init_status_result = result[0] if result else {}
877
- current_product_data.update(
878
- {
879
- "New Feature Info": init_status_result.get("new_feature_info"),
880
- "New Feature Info Str": init_status_result.get("new_feature_info_str"),
881
- "Feature Info": init_status_result.get("feature_info"),
882
- }
883
- )
884
996
 
885
- all_products_data[device.product.model] = current_product_data
997
+ current_product_data = {
998
+ "protocol_version": device.device_info.pv,
999
+ "product_nickname": device.product.product_nickname.name
1000
+ if device.product.product_nickname
1001
+ else "Unknown",
1002
+ }
1003
+ if device.v1_properties is not None:
1004
+ try:
1005
+ result: list[dict[str, Any]] = await device.v1_properties.command.send(
1006
+ RoborockCommand.APP_GET_INIT_STATUS
1007
+ )
1008
+ except Exception as e:
1009
+ click.echo(f" - Error processing device {device.name}: {e}", err=True)
1010
+ continue
1011
+ init_status_result = result[0] if result else {}
1012
+ current_product_data.update(
1013
+ {
1014
+ "new_feature_info": init_status_result.get("new_feature_info"),
1015
+ "new_feature_info_str": init_status_result.get("new_feature_info_str"),
1016
+ "feature_info": init_status_result.get("feature_info"),
1017
+ }
1018
+ )
1019
+
1020
+ product_data = device.product.as_dict()
1021
+ if product_data:
1022
+ current_product_data["product"] = product_data
1023
+
1024
+ all_products_data[model] = current_product_data
1025
+
1026
+ if record:
1027
+ if not all_products_data:
1028
+ click.echo("No device info updates needed.")
1029
+ return
1030
+ updated_device_info = {**existing_device_info, **all_products_data}
1031
+ device_info_path.parent.mkdir(parents=True, exist_ok=True)
1032
+ ordered_data = dict(sorted(updated_device_info.items(), key=lambda item: item[0]))
1033
+ with open(device_info_path, "w", encoding="utf-8") as f:
1034
+ yaml.safe_dump(ordered_data, f, sort_keys=False)
1035
+ click.echo(f"Updated {device_info_path}.")
1036
+ click.echo("\n--- Device Info Updates ---\n")
1037
+ click.echo(yaml.safe_dump(all_products_data, sort_keys=False))
1038
+ return
886
1039
 
887
1040
  if all_products_data:
888
1041
  click.echo("\n--- Device Information (copy to your YAML file) ---\n")
889
- # Use yaml.dump to print in a clean, copy-paste friendly format
890
1042
  click.echo(yaml.dump(all_products_data, sort_keys=False))
891
1043
 
892
1044
 
@@ -919,19 +1071,19 @@ def update_docs(data_file: str, output_file: str):
919
1071
  for model, data in product_data_from_yaml.items():
920
1072
  # Reconstruct the DeviceFeatures object from the raw data in the YAML file
921
1073
  device_features = DeviceFeatures.from_feature_flags(
922
- new_feature_info=data.get("New Feature Info"),
923
- new_feature_info_str=data.get("New Feature Info Str"),
924
- feature_info=data.get("Feature Info"),
925
- product_nickname=data.get("Product Nickname"),
1074
+ new_feature_info=data.get("new_feature_info"),
1075
+ new_feature_info_str=data.get("new_feature_info_str"),
1076
+ feature_info=data.get("feature_info"),
1077
+ product_nickname=data.get("product_nickname"),
926
1078
  )
927
1079
  features_dict = asdict(device_features)
928
1080
 
929
1081
  # This dictionary will hold the final data for the markdown table row
930
1082
  current_product_data = {
931
- "Product Nickname": data.get("Product Nickname", ""),
932
- "Protocol Version": data.get("Protocol Version", ""),
933
- "New Feature Info": data.get("New Feature Info", ""),
934
- "New Feature Info Str": data.get("New Feature Info Str", ""),
1083
+ "product_nickname": data.get("product_nickname", ""),
1084
+ "protocol_version": data.get("protocol_version", ""),
1085
+ "new_feature_info": data.get("new_feature_info", ""),
1086
+ "new_feature_info_str": data.get("new_feature_info_str", ""),
935
1087
  }
936
1088
 
937
1089
  # Populate features from the calculated DeviceFeatures object
@@ -940,7 +1092,7 @@ def update_docs(data_file: str, output_file: str):
940
1092
  if is_supported:
941
1093
  current_product_data[feature] = "X"
942
1094
 
943
- supported_codes = data.get("Feature Info", [])
1095
+ supported_codes = data.get("feature_info", [])
944
1096
  if isinstance(supported_codes, list):
945
1097
  for code in supported_codes:
946
1098
  feature_name = str(code)
@@ -954,10 +1106,10 @@ def update_docs(data_file: str, output_file: str):
954
1106
  """Writes the data into a markdown table (products as columns)."""
955
1107
  sorted_products = sorted(product_features.keys())
956
1108
  special_rows = [
957
- "Product Nickname",
958
- "Protocol Version",
959
- "New Feature Info",
960
- "New Feature Info Str",
1109
+ "product_nickname",
1110
+ "protocol_version",
1111
+ "new_feature_info",
1112
+ "new_feature_info_str",
961
1113
  ]
962
1114
  # Regular features are the remaining keys, sorted alphabetically
963
1115
  # We filter out the special rows to avoid duplicating them.
@@ -18,6 +18,8 @@ class ZeoState(RoborockEnum):
18
18
  cooling = 8
19
19
  under_delay_start = 9
20
20
  done = 10
21
+ aftercare = 12
22
+ waiting_for_aftercare = 13
21
23
 
22
24
 
23
25
  class ZeoProgram(RoborockEnum):
@@ -554,6 +554,11 @@ class DeviceFeatures(RoborockBase):
554
554
  metadata={"product_features": [ProductFeatures.MOP_SHAKE_MODULE, ProductFeatures.MOP_SPIN_MODULE]}
555
555
  )
556
556
 
557
+ # Raw feature info values from get_init_status for diagnostics
558
+ new_feature_info: int = field(default=0, repr=False)
559
+ new_feature_info_str: str = field(default="", repr=False)
560
+ feature_info: list[int] = field(default_factory=list, repr=False)
561
+
557
562
  @classmethod
558
563
  def from_feature_flags(
559
564
  cls,
@@ -571,9 +576,17 @@ class DeviceFeatures(RoborockBase):
571
576
  # RobotNewFeatures = new_feature_info
572
577
  # newFeatureInfoStr = new_feature_info_str
573
578
  # feature_info =robotFeatures
574
- kwargs: dict[str, Any] = {}
579
+ kwargs: dict[str, Any] = {
580
+ # Store raw feature info for diagnostics
581
+ "new_feature_info": new_feature_info,
582
+ "new_feature_info_str": new_feature_info_str,
583
+ "feature_info": feature_info,
584
+ }
575
585
 
576
586
  for f in fields(cls):
587
+ # Skip raw feature info fields (already set above)
588
+ if f.name in ("new_feature_info", "new_feature_info_str", "feature_info"):
589
+ continue
577
590
  # Default all features to False.
578
591
  kwargs[f.name] = False
579
592
  if not f.metadata:
File without changes