Repository navigation
Description
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, subscribesrr/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] closedfor 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_credentialsaccepts 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
- Run the stack with at least one vacuum and a current onboarding/cloud capture.
- Set up the HA Roborock integration (or any python-roborock client) using the local email/PIN - it receives the current snapshot
rriot. - Log into the patched official Android app with the same email/PIN - the login response carries the same
rriot. - 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.
- 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
NicolasBonduel commentedon Nov 8, 2026
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 freshtoken/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: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 more32109).So the remaining upstream gaps are operational rather than missing-implementation:
Happy to close this if it's considered user-error; wanted to leave the mechanism documented either way.