deebot_client.mqtt_client uses user_id + JWT (not SN + 8-char SYSINFO). Probed mq-na.ecouser.net:443 with credentials from the user_access_token returned by the HTTP login flow. Confirmed: - MQTT connect with user_id + JWT: WORKS - Subscribed to iot/atr/+/did/class/resource/j, iot/p2p topics - Bot was mid-clean when probed: live onMapTrace, onPos, onMinorMap, onStats stream flowing - onStats area=8 time=420, mid=1394981773 -- bot is real and active - Clean command via p2p topic was published (rc=0) but the bot was already in a clean cycle, so the new command is queued Why earlier probes timed out: bot was between cleaning cycles. With the bot actively running, MQTT is live and fast. The 'we are missing the password' question was about the WRONG password. 8-char SYSINFO password is for the bot's own MQTT connection. A controller (Yeedi app, Node-RED, our HA integration) uses the user's JWT. |
||
|---|---|---|
| probes_archive | ||
| src/yeedi_protocol | ||
| tests | ||
| .gitignore | ||
| probe_950_url.py | ||
| probe_1013.py | ||
| probe_all_cmds.py | ||
| probe_appsvr.py | ||
| probe_clean.py | ||
| probe_clean_real.py | ||
| probe_correct_body.py | ||
| probe_deebot_client.py | ||
| probe_envelope.py | ||
| probe_full_body.py | ||
| probe_getbattery.py | ||
| probe_jmq.py | ||
| probe_jmq_8883.py | ||
| probe_jmq_tls.py | ||
| probe_jmq_v2.py | ||
| probe_lg_endpoints.py | ||
| probe_map_endpoints.py | ||
| probe_map_iot.py | ||
| probe_minimal.py | ||
| probe_mq_443.py | ||
| probe_mq_broker.py | ||
| probe_mqtt_du.py | ||
| probe_mqtt_jwt.py | ||
| probe_mqtt_minimal.py | ||
| probe_mqtt_modern.py | ||
| probe_mqtt_passwords.py | ||
| probe_mqtt_proto.py | ||
| probe_mqtt_stable.py | ||
| probe_mqtt_usernames.py | ||
| probe_multi_clean.py | ||
| probe_ngiot_control.py | ||
| probe_ngiot_final.py | ||
| probe_ngiot_hosts.py | ||
| probe_ngiot_sst.py | ||
| probe_ngiot_v2.py | ||
| probe_ngiot_verbose.py | ||
| probe_online_check.py | ||
| probe_online_state.py | ||
| probe_online_status.py | ||
| probe_pim_map.py | ||
| probe_reachability.py | ||
| probe_real_clean.py | ||
| PROBE_RESULTS.md | ||
| probe_sn_sst.py | ||
| probe_soundlocate.py | ||
| probe_sst_devmgr.py | ||
| probe_sst_http.py | ||
| probe_wake.py | ||
| probe_wake_v2.py | ||
| PROTOCOL.md | ||
| pyproject.toml | ||
| pytest.ini | ||
| README.md | ||
yeedi-protocol
A clean, dependency-light Python library that reverse-engineers the private Yeedi/Ecovacs cloud protocol, with live tests as the source of truth.
The Yeedi mobile app talks to the cloud via undocumented REST endpoints that aren't part of any public API. The behavior was pieced together from:
mrbungle64/ecovacs-deebot.js(the only actively maintained client)bmartin5692/sucks(older Python RE work)- Live probing against
gl-{country}-api.yeedi.comwith real credentials
The Home Assistant integration at Mark/yeedi_awmbuild consumes this
library. Building it as a separate package lets us prove the protocol
with a clean test runner and decouple the HA scaffolding from the
"what does the server actually want" work.
Layout
src/yeedi_protocol/
auth.py # 3-step login + 1013 device-verification recovery
crypto.py # authSign MD5 + RSA encrypt for verification
discovery.py # list_user_devices() + probe helpers
cli.py # yeedi-probe {login,discover,recovery,full}
errors.py
tests/
live/ # scripts that hit the real server, gated on env vars
unit/ # deterministic tests with mocked HTTP
Run live tests
export YEEDI_EMAIL='you@example.com'
export YEEDI_PASSWORD='your-password'
export YEEDI_COUNTRY='US' # default if unset
uv run pytest -v
A live test only runs if both env vars are set; otherwise pytest skips it. Every live test prints the actual server response so a failure shows you exactly what came back, not a guess.
What's verified live (2026-10-09)
| Step | Endpoint | Status |
|---|---|---|
| 1. user/login | GET gl-{cc}-api.yeedi.com/v1/private/.../user/login | ✅ 0000 |
| 2. getAuthCode | GET gl-{cc}-openapi.yeedi.com/v1/global/auth/getAuthCode | ✅ 0000 |
| 3. loginByItToken | POST api-app.dc-{cc}.ww.ecouser.net/api/users/user.do | ✅ result=ok |
| 4. GetDeviceList | POST {api-app.dc-{cc}.ww or portal-{cc}}/users/user.do | ✅ result=ok — returns bound bots |
| 5. Commands | POST iot/devmanager.do | ✅ body shape verified, 10s timeout — times out cleanly when bot is offline |
| 5b. GetCleanLogs | POST {portal_host}/lg/log.do | ✅ result=ok, works even when bot is offline |
Bot-offline behavior: iot/devmanager.do holds the HTTP connection
open waiting for the device to ACK. When the bot is on the dock and idle,
calls time out cleanly with CommandError. Catch it and treat the
device as unavailable until the next successful call. The existing HA
yeedi_c12_cloud integration has the same behavior.
await async_login(session, email, password, country, device_id) returns
(Auth, [Device]) with the user's actual vacuum(s). See PROTOCOL.md
for the full writeup.