Skip to content

feat(mower): add typed status models and protocol enums - #1002

Merged
allenporter merged 3 commits into
mainfrom
codex/mower-status-model
Nov 1, 2026
Merged

allenporter merged 3 commits into
mainfrom
codex/mower-status-model

Conversation

@Lash-L

@Lash-L Lash-L commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator

Mower status fields currently expose mostly raw integers. Add typed status enums with protobuf names and integer wire codes, plus a typed GPS coordinate model. Parsing accepts integer codes, decimal strings and protobuf names, with explicit fallback members for unknown firmware values.

Keep compact mowing states separate from detailed robot task states. The scalar DPS 123 field uses the detailed domain observed in the captured RockNeo Q105 status. Enum values and GPS fields were checked against RR_API's mower schema sources.

Preserve the existing RoborockMowerStateCode export, including all 60 original member names and values, and retain MowerStatus.from_dps() for mixed integer/string DPS keys. Tests move into the matching tests/data/mower/ directory and cover those entry points alongside typed parsing and round trips.

API behavior: MowerStatus fields now contain the new typed enums and GPS model; as_dict() serializes those enums using protobuf names. Retaining the old enum export does not make new status values instances of that legacy enum.

Validation:

  • pytest -q -p no:cacheprovider --benchmark-disable: 1,721 passed, 53 expected failures, 11 deselected; 99 snapshots passed. Benchmark tests run once without timing measurements.
  • pre-commit run --all-files: all hooks passed, including Ruff, Mypy and Codespell.
  • Confirmed all existing legacy enum names and integer values are unchanged.

This is the first mower support PR and contains only data models, enum mappings and their tests. No live mower commands were sent.

Add protobuf-named status enums with integer wire codes, explicit unknown fallbacks, and a typed GPS coordinate model. Keep compact and detailed task state domains separate, using the captured detailed domain for scalar DPS 123.

Retain RoborockMowerStateCode with all existing names and values and preserve MowerStatus.from_dps. Cover typed parsing, serialization, unknown values, and the retained entry points in the mirrored mower data tests.
Copilot AI balanced review requested due to automatic review settings October 31, 2026 01:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@codecov

codecov Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
roborock/data/mower/mower_code_mappings.py 100.00% <100.00%> (ø)
roborock/data/mower/mower_containers.py 100.00% <100.00%> (ø)
tests/data/mower/test_mower_code_mappings.py 100.00% <100.00%> (ø)
tests/data/mower/test_mower_containers.py 100.00% <100.00%> (ø)

... and 12 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Lash-L Lash-L left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated code review by Gemini 4 Argon in Antigravity using the /code-review skill (.agents/skills/code-review/SKILL.md / AGENTS.md).

Summary

Well-scoped first mower support PR replacing raw int / Any fields on MowerStatus with strongly typed MowerEnum (RoborockModeEnum) classes and MowerGpsCoordinate (RoborockBase), while retaining the legacy RoborockMowerStateCode export and MowerStatus.from_dps() factory.

  • Layer 1 — Strongly Typed Models & Conformance: Eliminates Any on MowerStatus.offline_status and MowerStatus.gps_coordinate, replacing them with MowerOfflineStatus and MowerGpsCoordinate(RoborockBase).
  • Layer 3 — Wire Protocol & Enum Resilience: Separates compact (MowerMowState) and detailed (MowerDetailState) task state numeric domains, preserves wire misspellings (MAP_ERASEING), and ensures every status enum resolves unrecognized firmware codes to UNKNOWN without crashing.
  • Layer 5 — Test Mirroring & Colocation: Moves tests/data/test_mower_containers.py into the mirrored tests/data/mower/test_mower_containers.py and tests/data/mower/test_mower_code_mappings.py suites with parameterized round-trip and boundary tests.

One minor item noted inline:

  1. MowerEnum.from_code Unreachable Fallback Branches (roborock/data/mower/mower_code_mappings.py#L129-L137): Every MowerEnum subclass defines UNKNOWN as its first member, leaving the loop continuation and raise ValueError branches unreachable (flagged by Codecov).

@Lash-L
Lash-L requested a review from allenporter October 31, 2026 01:53
@allenporter
allenporter merged commit fa1e62b into main Nov 1, 2026
10 checks passed

@allenporter allenporter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding some post-merge comments to consider for followup

class MowerErrorCode(MowerEnum):
"""User-facing error code reported by DPS 120."""

UNKNOWN = ("unknown", 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double check that you're happy with the no error case (e.g. MowerErrorCode.NONE isn't a thing, but None is. MowerErrorCode.UNKNOWN is an error, not none, etc.

class MowerGpsCoordinate(RoborockBase):
"""Mower GPS position."""

latitude: float | None = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add latitude and longitude and gpsCoordinate to redact keys in diagnostics.py?

"""Resolve a wire value by name, string value, or integer code."""
if (member := cls.from_any_optional(code)) is not None:
return member
return cast(Self, cls.UNKNOWN) # type: ignore[attr-defined]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you care about logging on unknown values? even at debug?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants