Skip to content

fix: fetch the latest user agreement version instead of hardcoding it - #960

Open
xiaoyaner0201 wants to merge 1 commit into
Python-roborock:mainfrom
xiaoyaner0201:fix/dynamic-agreement-version
Open

xiaoyaner0201 wants to merge 1 commit into
Python-roborock:mainfrom
xiaoyaner0201:fix/dynamic-agreement-version

Conversation

@xiaoyaner0201

Copy link
Copy Markdown

Problem

code_login_v4 sends a hardcoded user agreement version with the login request:

# Major and minor version are the user agreement version, we will need to see if this needs to be
# dynamic https://usiot.roborock.com/api/v3/app/agreement/latest?country=US
"majorVersion": 14,
"minorVersion": 0,

The server rejects a stale agreement version with code 3006, which is raised as
RoborockInvalidUserAgreement:

INFO:roborock.web_api:Login failed with the following context:
{'msg': 'user agreement version is not up-to-date', 'data': None, 'code': 3006}
roborock.exceptions.RoborockInvalidUserAgreement: User agreement must be accepted again -
or you are attempting to use the Mi Home app account.

Note the server's own message: "user agreement version is not up-to-date". The
problem is the version number, not the user's acceptance state.

Why the usual workaround cannot fix it

The documented workaround is to revoke and re-accept the agreement in the app. That
cannot clear this error, because nothing about the account is wrong — the constant sent
by this library is stale. This matches #784, where a user followed that advice
repeatedly across two months and several releases without success.

Measurement

14 is not the current version on any server today, and the version differs per server
and per country:

server ?country= latest majorVersion.minorVersion
cniot CN/US/DE/RU 19.0
usiot US 16.0
usiot CN/DE/RU 14.0
euiot any 15.1
ruiot any 15.0

Reproduce:

for host in cniot usiot euiot ruiot; do
  echo -n "$host -> "
  curl -s "https://$host.roborock.com/api/v3/app/agreement/latest?country=US" \
    | python3 -c "import json,sys;d=json.load(sys.stdin)['data'];print(d['majorVersion'],d['minorVersion'])"
done

Two details worth noting:

  • usiot with a non-US country still returns 14, which is likely why this went
    unnoticed for some users while failing for others.
  • minorVersion cannot be hardcoded to 0 either — euiot currently reports 15.1.

Fix

Query /api/v3/app/agreement/latest (the endpoint already named in the removed comment)
and send the version it returns. If the lookup fails, fall back to the previous constants
so that login is never blocked by this extra request.

Tests

Two regression tests added:

  • test_code_login_v4_uses_latest_agreement_version — asserts the version sent on login
    comes from the agreement endpoint. It asserts the observable request payload rather
    than the helper, so any correct implementation passes.
  • test_code_login_v4_agreement_version_fallback — asserts login still succeeds with the
    fallback constants when the lookup raises.

Verified by reverting only the production change and keeping the tests:

>       assert data["majorVersion"] == 19
E       assert 14 == 19
FAILED tests/test_web_api.py::test_code_login_v4_uses_latest_agreement_version

With the fix applied:

1035 passed, 92 snapshots passed in 47.16s
ruff check: All checks passed!
ruff format --check: 2 files already formatted
mypy roborock/web_api.py: Success: no issues found in 1 source file

Verified against a live account

Confirmed end-to-end on a cniot account that could not log in before this change:
login now succeeds and the device list loads. Before the fix the same account failed
with 3006 on every attempt.

Closes #784

`code_login_v4` sent a hardcoded `majorVersion: 14`/`minorVersion: 0` with the
login request. The server rejects a stale agreement version with code 3006,
which surfaces as `RoborockInvalidUserAgreement` ("User agreement must be
accepted again - or you are attempting to use the Mi Home app account").

The current version is not 14 on any server today, and it differs per server
and per country, so it cannot be hardcoded:

| server | latest majorVersion.minorVersion |
| ------ | -------------------------------- |
| cniot  | 19.0                             |
| usiot  | 16.0 (country=US)                |
| euiot  | 15.1                             |
| ruiot  | 15.0                             |

Because the value is stale server-side rather than account-side, accepting the
agreement again in the app cannot clear it, which is why the usual
revoke-and-re-accept workaround does not help.

Query `/api/v3/app/agreement/latest` (the endpoint already named in the removed
comment) and send the returned version, falling back to the previous constants
so a failed lookup never blocks login. `minorVersion` is taken from the response
too, since euiot currently reports 15.1.

@Lash-L Lash-L left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

So the reason I didn't do this was I did not know what happened if a new agreement version was added that the user had not agreed with if we would be able to login or if we would get a failure stating that we need to resign the agreement.

But diving into some of the code, I think this will be okay and work.

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.

"Error: User agreement must be accepted again..." - what does that mean and how to get over this?

2 participants