Skip to content

fix(api): answer /api/v1/checkAppAgreement with accepted agreements - #209

Merged
Lash-L merged 2 commits into
Python-roborock:mainfrom
SmarthomeElektroniker:fix/check-app-agreement-v1
Oct 31, 2026
Merged

Lash-L merged 2 commits into
Python-roborock:mainfrom
SmarthomeElektroniker:fix/check-app-agreement-v1

Conversation

@SmarthomeElektroniker

Copy link
Copy Markdown
Contributor

Problem

When a device page is opened, the Android app calls GET /api/v1/checkAppAgreement. The request ends up in the catch-all. The app deserializes the response as UserAgreementInfoV1:

{
  "userAndPrivacyAgree": true,
  "improvementPlanAgree": true,
  "recommendationAgree": true,
  "userPluginAgreementList": [
    {"deviceId": "<duid>", "type": 1, "version": 1, "extra": ""}
  ]
}

Every field has a default, so the catch-all object does not fail. It silently becomes "nothing agreed" with an empty plugin agreement list. The app log then shows

RR/UserPreferenceRepository: checkAgreementV1 pluginAgreements:
RR/UserPreferenceRepository: getPluginAgreements no data <duid>

Then the device plugin stops making requests, and the map stays on "loading". The plugin's last native calls before it goes silent are getProductAgreements, getPluginAgreements (empty list) and readFileListAtPath. After that, the plugin sends no further request.

The field names come from the app's UserAgreementInfoV1 serializer and from UserPreferenceRepository$getPluginAgreements (deviceId, type, version, extra).

Fix

New route routes/api/v1/agreement.py (check_app_agreement_v1):

  • reports the three app agreements as accepted
  • reports one plugin agreement per home device and agreement type

Caveat: we do not know the full set of valid type values. The app keeps this list as a raw JSON element and passes it to the plugin. Types 1–3 with version 1 are enough for the plugin to get past this check. If you know the real values from a cloud capture, they are easy to swap in (PLUGIN_AGREEMENT_TYPES).

Tests

tests/test_check_app_agreement.py

  • all agreements accepted, entries with the expected keys and types for every paired device
  • no paired devices gives an empty plugin agreement list

Full suite: passing.

Live check (Oct 2026): verified together with the other fixes of this series (all based on current main) against the Android app 4.60.06 and a real Q7 Max (roborock.vacuum.a38). The app called /api/v1/checkAppAgreement (now served by the new route). The device plugin got past its agreement check and went on to fetchDeviceLocation, with no privacy dialog.

Notes

This route removes the server-side cause of the "no data" log. Map loading can also depend on app-side factors I could not isolate, so I can't promise that this route alone makes the map load in every setup.

🤖 Generated with Claude Code

The Android app requests GET /api/v1/checkAppAgreement when a device page is
opened and deserializes it as UserAgreementInfoV1:

    {userAndPrivacyAgree, improvementPlanAgree, recommendationAgree,
     userPluginAgreementList}

All fields have defaults, so the generic catch-all object deserializes
silently into "nothing agreed" with an empty plugin agreement list. The app
then logs "getPluginAgreements no data <duid>" and the device plugin stops
before requesting the map.

Add a dedicated route that reports the app agreements as accepted and one
plugin agreement entry ({deviceId, type, version, extra}) per home device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# valid type values is not known: the Android app keeps this list as a raw
# JSON element and hands it to the device plugin. Reporting types 1-3 with
# version 1 is enough for the plugin to continue past its agreement check.
PLUGIN_AGREEMENT_TYPES = (1, 2, 3)

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.

I believe this should be strings.

PLUGIN_AGREEMENT_TYPES = ("USER_AGREEMENT", "PRIVACY_POLICY")

I believe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right, thanks. I checked the app: UserPreferenceRepository.getPluginAgreements reads each entry's type as a string (JsonPrimitive.content), and both the a38 plugin bundle and the app's AgreementInfo bean use the constants USER_AGREEMENT and PRIVACY_POLICY. The placeholder integers only worked because the plugin didn't reject the unknown values.

Changed in effc85a: PLUGIN_AGREEMENT_TYPES = ("USER_AGREEMENT", "PRIVACY_POLICY"), test updated accordingly.

The Android app reads each userPluginAgreementList entry's "type" as a
string (JsonPrimitive.content in UserPreferenceRepository.getPluginAgreements)
and the device plugin bundle as well as the app's AgreementInfo bean use the
constants USER_AGREEMENT and PRIVACY_POLICY. Report those two instead of the
placeholder integers 1-3, as suggested in review.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Lash-L
Lash-L merged commit 02a1770 into Python-roborock:main Oct 31, 2026
3 checks passed
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.

2 participants