Skip to content

Per-login sessions: app + HA integration share one rriot, identical MQTT clientIds disconnect each other (map never loads) #240

Closed

Description

@NicolasBonduel

What happened?

The local email/PIN login returns the single stored cloud-snapshot user_data to every client that logs in (build_login_data_response -> load_cloud_user_data in routes/auth/service.py). Any two clients that log in while the snapshot holds the same generation therefore receive an identical rriot - and derive identical MQTT usernames and identical MQTT client IDs (both sides compute md5(rriot.u + ":" + rriot.k)[2:10]).

MQTT brokers drop the older session when a second client presents the same client ID. With two app-protocol clients logged in against the same stack, each one's reconnect evicts the other:

  • The patched official Roborock app (per docs/roborock_app.md) connects, subscribes rr/m/o/<u>/<user>/<duid>, and starts receiving dps responses - then is evicted within seconds-to-minutes, repeatedly, whenever the other client's connection cycles.
  • With the connection repeatedly torn down mid-map, the app's device screen never gets past "Getting Info" / "Loading Map", and the plugin's status requests are cut mid-flight.
  • Instrumented app-side logging shows the client-side view each time: connect success followed seconds later by MqttConnectionManager: Connection lost (32109) - java.net.SocketException: Software caused connection abort.
  • Server-side, the MQTT proxy log shows the two clients CONNECTing with identical clientId/username, and [conn N] closed for the loser of each collision.
  • A third client holding an older-generation identity (logged in before the snapshot was refreshed, still valid because verify_user_mqtt_credentials accepts all persisted sessions) runs unaffected next to the other two - which isolates the failure to identity sharing rather than concurrency or load.

This reproduces with the Home Assistant "Roborock" integration (python-roborock) as the second client, but any two app-protocol clients that log in close together collide the same way.

Steps to reproduce

  1. Run the stack with at least one vacuum and a current onboarding/cloud capture.
  2. Set up the HA Roborock integration (or any python-roborock client) using the local email/PIN - it receives the current snapshot rriot.
  3. Log into the patched official Android app with the same email/PIN - the login response carries the same rriot.
  4. Open the app's device screen and watch the MQTT proxy logs: both clients CONNECT with the same client ID; every reconnect of one drops the other's established session. The app's map never finishes loading; HA entities flap.
  5. Force-stop either client - the other immediately stabilizes and its map loads.

Vacuum model

Roborock S7 MaxV (roborock.vacuum.a27), firmware 02.59.36

Server / image version

Home Assistant add-on 1.2.1 (HAOS); also present in current main (checked through #209)

Relevant logs

Server MQTT proxy (identifiers truncated; both clients share the same credentials):

TLS handshake ok from <ip> (TLSv1.3)
[conn 20 c2b] CONNECT len=50 ... clientId=ab12cd34 username=ab12cd34   <- official app
[conn 20 b2c] CONNACK len=4 hex=20020000
[conn 21 c2b] CONNECT len=50 ... clientId=ab12cd34 username=ab12cd34   <- python-roborock client, same creds
[conn 20] closed                                                        <- broker dropped the app

Instrumented app (internal MqttConnectionManager logging):

connect success true duration 523
subscribe success: [rr/m/o/<u>/<user>/<duid>]
connection lost (32109) - java.net.SocketException: Software caused connection abort

Suggested direction

The pieces for a proper fix already exist: ProtocolAuthStore persists multiple sessions (PR #204 raised capacity to 64) and verify_user_mqtt_credentials already iterates every stored session - the login handler just never mints anything. A small change in the login path - copy the snapshot user_data, replace rriot.u (random 22-hex) and rriot.s (random 12-hex) while keeping rriot.h/rriot.k (so hawk signing and the dps codec stay consistent), and upsert_user_data the minted session before returning it - would give each client its own MQTT identity while leaving older sessions valid.

Activity

  1. NicolasBonduel commented on Nov 8, 2026

    @NicolasBonduel
    Author

    Update after further testing:

    The stack does mint per-login sessions — ProtocolAuthStore.issue_local_session (present since #10, also in 1.2.1) hands the local email/PIN code-login a fresh token/u/s/h/k, so my "Suggested direction" above was wrong about the missing machinery. Sorry for the noise; the observed collision is real but has a different shape:

    • One client (the patched official app) had logged in back when its identity was the MITM-synced/cloud one, and kept running on that shared identity — it had simply never re-logged through the local email/PIN path after the stack was set up.
    • A second client (HA / python-roborock) used that same snapshot identity.
    • Both derived the same MQTT username and the same client ID, so each reconnect evicted the other — the app's device screen could never finish loading the map.

    Reproduced the resolution: re-logging the app once through the local email/PIN form minted a fresh identity (rr/m/o/<new-u>/<new-user>/<duid>), and the two clients now coexist on the broker with zero disconnect churn (app instrumented logs show a stable session, no more 32109).

    So the remaining upstream gaps are operational rather than missing-implementation:

    1. Nothing tells you a stale pre-mint identity is colliding — from the outside it just looks like random connection churn (ours looked like the app "never connects").
    2. Possibly worth documenting: clients that authenticated in the MITM-sync era should re-login once to pick up a minted, non-shared identity.

    Happy to close this if it's considered user-error; wanted to leave the mechanism documented either way.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions