Repository navigation
Conversation
allenporter
left a comment
There was a problem hiding this comment.
Automated code review by Gemini 4 Argon in Antigravity using the /review skill (.agents/skills/review/SKILL.md / AGENTS.md).
Summary
Great work breaking the camera livestream work into focused, zero-dependency increments! The RFC 5245/8839/8445 ICE candidate parsing, repr=False credential redaction on CameraTurnServer, and test coverage are well thought out.
Below are a few architectural and typing items to align with our repository conventions and conformance suite:
- Priority 1 — Module Placement &
RoborockBaseConformance: Defining data models in top-levelroborock/camera_signaling.pyand re-exporting them inroborock/data/camera.pycausestests/conformance/test_model_conformance.py(discover_dataclasses(roborock.data)) to skip them becausecls.__module__does not start withroborock.data. As a result,CameraIceCandidatedoes not subclassRoborockBase. Defining the data models directly inroborock/data/(orroborock/data/v1/) and placing the V1 signaling codecs inroborock/protocols/keeps the top-level package clean and ensures conformance test coverage. - Priority 1 — Trust Type Annotations & Concrete Signatures: Several functions perform defensive
isinstancechecks on statically typed parameters (CameraQuality.from_str,parse_ice_candidate,hash_camera_password,encode_ice_candidate), and outgoing encoders accept loosedict[str, Any]union inputs rather than concrete types. - Priority 3 — Linear Wire Decoding in
decode_ice_candidates:decode_ice_candidatesmixes wire payload decoding (get_device_ice) with raw SDP string parsing via anitem_str.startswith("ey")heuristic.
| password hashing, and base64-JSON payload encoding/decoding. | ||
| """ | ||
|
|
||
| from __future__ import annotations |
There was a problem hiding this comment.
Avoid from __future__ import annotations (AGENTS.md §1):
Python 3.11+ natively supports Self and X | Y unions at runtime. Including from __future__ import annotations turns dataclasses.fields(cls) .type attributes into strings ('list[str]', 'str | None'), which breaks RoborockBase._convert_to_class_obj runtime reflection when RoborockBase.from_dict is used. Please remove from __future__ import annotations here and in roborock/data/camera.py.
| def from_str(cls, value: str) -> Self: | ||
| """Parse CameraQuality case-insensitively.""" | ||
| if not isinstance(value, str): | ||
| raise TypeError(f"Expected str for camera quality, got {type(value).__name__}") |
There was a problem hiding this comment.
Trust Type Annotations (AGENTS.md §1):
Prohibit defensive runtime isinstance checks on statically typed parameters (value: str). The same applies to parse_ice_candidate(candidate_str: str) (line 203) and hash_camera_password(password: str) (line 320).
| credential=pwd_val, | ||
| ) | ||
|
|
||
| def to_dict(self) -> dict[str, Any]: |
There was a problem hiding this comment.
Redundant to_dict() vs RoborockBase.as_dict() (AGENTS.md §1):
CameraTurnServer subclasses RoborockBase, whose inherited as_dict() already returns {"urls": [...], "username": ..., "credential": ...} while omitting None fields. Consider using as_dict() rather than introducing a duplicate to_dict() method.
|
|
||
|
|
||
| @dataclass | ||
| class CameraIceCandidate: |
There was a problem hiding this comment.
Subclass RoborockBase & Module Placement (AGENTS.md §1, tests/conformance/test_model_conformance.py):
CameraIceCandidateis re-exported underroborock.data, so it must subclassRoborockBase.- Because
CameraIceCandidate,CameraTurnServer, andCameraQualityare defined inroborock.camera_signalingand only re-exported inroborock/data/camera.py,discover_dataclasses(roborock.data)skips them (cls.__module__is"roborock.camera_signaling").
Please define the domain models directly in roborock/data/camera.py (or roborock/data/v1/) and place the V1 protocol serializers/parsers in roborock/protocols/ (with mirrored tests under tests/data/ and tests/protocols/).
| return hashlib.md5(password_str.encode("utf-8"), usedforsecurity=False).hexdigest() | ||
|
|
||
|
|
||
| def encode_sdp_offer(sdp: str | dict[str, Any]) -> str: |
There was a problem hiding this comment.
Strongly Type Public Boundaries (AGENTS.md §1):
Avoid loose dict[str, Any] union parameters on outgoing encoders (encode_sdp_offer and encode_ice_candidate at line 422). Accepting a concrete sdp: str (or a RoborockBase model) for encode_sdp_offer and candidate: CameraIceCandidate for encode_ice_candidate keeps the API strongly typed and eliminates the polymorphic isinstance / int(...) coercion branches.
| return base64.b64encode(json_bytes).decode("ascii") | ||
|
|
||
|
|
||
| def decode_sdp_answer(payload: str | dict[str, Any] | list[Any]) -> dict[str, str] | None: |
There was a problem hiding this comment.
Return Concrete Model Instead of Loose dict[str, str] (AGENTS.md §1):
Consider returning a @dataclass subclassing RoborockBase (e.g., CameraSdpDescription(sdp: str, type: str)) or str | None rather than a raw dict[str, str].
| except (binascii.Error, ValueError, json.JSONDecodeError, UnicodeDecodeError, TypeError) as err: | ||
| # If this was clearly an attempted base64 JSON payload (starts with 'ey'), | ||
| # or valid base64 that decoded to non-dict, do not fall back to treating it as raw candidate | ||
| if item_str.startswith("ey") or ( |
There was a problem hiding this comment.
Fragile startswith("ey") Heuristic in decode_ice_candidates (AGENTS.md §2):
decode_ice_candidates attempts to accept both wire payloads from get_device_ice (base64-encoded JSON strings) and raw unencoded SDP candidate strings by guessing whether item_str.startswith("ey").
Additionally, if a raw candidate string happens to have a valid base64 character prefix/length and fails in json.loads() with json.JSONDecodeError, not isinstance(err, binascii.Error) and not isinstance(err, UnicodeDecodeError) is True, so it raises Failed to decode base64 JSON ICE candidate instead of falling back.
Since parse_ice_candidate() already handles raw SDP candidate strings, decode_ice_candidates() should strictly decode the wire format (get_device_ice base64 JSON strings / {"dev_ice": [...]} envelope) without fallback guessing.
Summary
Implements PR 2 of the decoupled Roborock vacuum live camera streaming series (following #980, #764, #738).
This PR introduces pure Python data structures, RFC-compliant ICE candidate parsing/formatting utilities, password hashing, and WebRTC signaling serializers for Roborock camera live streaming. It adds zero external runtime dependencies (
aiortcis deferred to PR 4).Scope & Components
Camera Signaling Models:
CameraQuality(StrEnum:HD,SD).CameraTurnServerdataclass (urls,username,credentialwithrepr=Falsefor security; includesfrom_dicthelper validating TURN/STUN schemes).CameraIceCandidatedataclass (encapsulates foundation, component, protocol, priority, ip, port, candidate_type, related_address, related_port, tcp_type, sdp_mid, sdp_mline_index, and arbitrary SDP extensions).RFC-Compliant ICE Candidate Parser & Formatter:
parse_ice_candidate: RFC 5245 / 8839 / 8445 compliant parser for SDP candidate attribute lines (candidate:...)..local) addresses.active,passive,so).ufrag,generation,network-id).CameraIceCandidate.to_sdp()andto_signaling_dict().Security & Password Hashing:
hash_camera_password: Derives the lowercase hexadecimal MD5 digest of numeric camera PIN/pattern digits with strict ASCII validation and FIPS-compliantusedforsecurity=False.WebRTC Payload Serialization:
encode_sdp_offer&decode_sdp_answer: Compact base64-encoded UTF-8 JSON packaging forsend_sdp_to_robot/get_device_sdp, handling"retry"pending states while preserving SDP CRLF line integrity.encode_ice_candidate&decode_ice_candidates: Base64-encoded UTF-8 JSON packaging forsend_ice_to_robot/get_device_ice, with schema validation and optionalignore_invalidresilience for remote candidate streams.Re-exports & Packaging:
roborock.camera_signaling,roborock.data.camera, androborock.data.Validation
uv run pytest tests/test_camera_signaling.py).uv run ruff checkanduv run ruff format --check.uv run mypy.sol 6.1and Claudeopus 5.5).