node-dreame
v0.1.4
Published
Node.js client for the Dreame native cloud (Dreamehome app backend). Control Dreame robot vacuums without going via Home Assistant or Xiaomi Mi cloud.
Maintainers
Readme
node-dreame
Node.js client for the Dreame native cloud — the backend behind the Dreamehome mobile app. Control Dreame robot vacuums from Node, without going via Home Assistant or Xiaomi Mi cloud.
Status: pre-alpha. Auth flow is being reverse-engineered. Public API will change. Do not use in production yet.
Why this exists
Most existing Dreame integrations (notably Tasshack/dreame-vacuum) talk to Xiaomi Mi cloud, which only works for robots paired with the Mi Home / Xiaomi Home app. Robots paired with the Dreamehome app live on a different backend and aren't reachable via that path.
This library targets that gap.
Scope
- In scope: Dreamehome cloud auth (email/password), device discovery, status polling, command dispatch, MQTT live updates, room-aware cleaning.
- Out of scope (for now): Mi cloud, the binary map renderer, Home Assistant integration.
Install
npm install node-dreameRequires Node.js 18 or newer.
Usage
import { DreameClient } from "node-dreame";
const dreame = new DreameClient({
email: "[email protected]",
password: "***",
region: "eu", // or "us", "cn", ...
});
await dreame.login();
const devices = await dreame.getDevices();
const vacuum = dreame.getVacuum(devices[0]);
// Inspect what the hardware supports before rendering UI / building requests:
if (vacuum.capabilities.canMop) {
console.log("Mop supported. Water levels:", vacuum.capabilities.supportedWaterVolumes);
}
await vacuum.refresh();
await vacuum.locate();CommonJS:
const { DreameClient } = require("node-dreame");Live updates over MQTT
Open an MQTT subscription to receive properties_changed, OTA progress, task-complete events, and live-map frames as the device pushes them:
await vacuum.watch();
vacuum.on("change", (state) => console.log(state));
vacuum.on("taskComplete", (record) => console.log("done:", record));The MQTT channel is passive — the device only pushes when state changes, so a quiet idle window can look indistinguishable from a broken subscription. Use the first-class verifier to remove the ambiguity:
const r = await vacuum.verifyMqtt();
if (r.reason !== "ok") { console.error("subscription not healthy:", r.reason); }r.reason discriminates: "ok" (broker echoed our trigger write back), "no-echo" (no echo within timeout — device may be genuinely unreachable or just unresponsive to the no-op), "not-watching" (watch() wasn't called). The MQTT echo is the source of truth — the HTTP layer's code 80001 ("device offline") is ignored because it's a false negative on healthy devices (the cloud's HTTP-side ACK waiter often times out while the device is actually executing the action and echoing state back over MQTT — see DreameDeviceOfflineError for details).
For the live-map case during an active cleaning task, don't sit waiting for the first 'map' event — actively provoke one:
const data = await vacuum.map.whenReady(); // live channel, resolves on next pushwhenReady(timeoutMs?) resolves with the next decoded MapData, kicking requestIFrame() to bootstrap. Default timeout 30000ms. The same vacuum.map exposes requestIFrame(opts?) directly when you need the underlying action without the wait.
For a static floor plan (when the device is idle on the dock and won't push live frames), use the saved-map path instead — it doesn't depend on the device pushing anything:
const list = await vacuum.fetchSavedMapList();
const active = list?.maps.find((m) => m.mapId === list.activeMapId);
const data = active?.data; // a full MapData for the current floorBuilding a web app on top of node-dreame
node-dreame runs in a Node.js process and emits events. If you want a
browser front-end (e.g. a live map UI), put a thin bridge between
node-dreame and your browser. Two reference adapters live in
examples/, both ~120 lines, copy-and-adapt:
examples/server-sse.ts— zero-deps Server-Sent Events stream +POST /actions/<name>. Simplest. Use when one-way live updates plus discrete commands is enough.examples/server-websocket.ts— bidirectional WebSocket usingws. Use when the browser needs to push back frequently (action invocations, cursor positions, etc.).
Both forward vacuum.map, vacuum.on('change'), and vacuum.on('ota')
to all connected clients with a tagged { type, data } envelope.
Live map streaming on its own (no server) is in
examples/live-map-stream.ts.
The map shape (MapData) is documented in
docs/live-map-roadmap.md. Coordinates
are raw mm in the device's world frame — the browser does the
viewport transform.
Supported devices
Developed against a Dreame r2532a (X50 Ultra Complete, EU region, firmware 4.3.9_2199). Other models may work — the auth and transport layer should be model-agnostic — but the property/action catalogue in miot-spec.ts is partly verified on r2532a and partly inherited from Tasshack/dreame-vacuum (older Dreames on Mi cloud).
Coverage status
This is a partial mapping, not a complete one. We've prioritised the surface most useful for home-automation integration (dock settings, OTA, schedules, a handful of actions), but large parts of the device's feature set haven't been observed yet.
Roughly:
- Well-mapped: auth + transport, MQTT event channels, dock settings, OTA flow, the global Custom-mode schedule format, basic battery/charge/state.
- Partly mapped: MIoT state enum (we have all the keyDefine translations, but only a subset have been observed in real transitions); FEATURE_CONFIG_JSON keys (3 of ~36 confirmed by toggle, the rest documented by name only).
- Hardly touched: actual cleaning runs, room-targeted cleaning behaviour, per-room schedule packing, the
0xC249middle bits of the global Custom-mode int, voice configuration, DND scheduling, error-code catalogue, AI object-detection class IDs, the siid 6 / siid 99 binary blobs (likely live map + telemetry), and the many siid 4 piids we never observed move.
Each entry in src/miot-spec.ts is annotated:
// VERIFIED <date>— observed working on r2532a// ASSUMED from <source>— borrowed from another project, not yet confirmed on r2532a
If a behaviour you care about isn't VERIFIED, treat it as a guess. If you exercise something new, please contribute the finding back — the methodology is documented in docs/spec-discovery-methodology.md and the long-running logger in examples/log-events.ts makes it cheap to add observations.
Known specifically-verified pieces
- Auth + device discovery + MQTT subscription
- Typed event channels:
properties_changed,props(incl. OTA),_otc.info - Property reads (state, error, battery, charging, suction, water, cleaning_mode raw, task_status raw, volume, consumables, firmware build, serial, timezone, off-peak charging window, DND windows, feature toggles JSON, version metadata)
- Property writes (round-trip verified)
- Actions:
LOCATE,TEST_SOUND,CLEAR_WARNINGonly - Full OTA cycle (download → install → reboot → re-online → version flip)
- All dock settings reachable from the Dreamehome app's "Base Station" menu (mop wash temp/water level/wetness, drying mode, hair compression, smart-mode master, mast control, auto-empty frequency)
- All cleaning behaviour settings reachable from the app's "Cleaning Settings" menu (carpet handling mode + sub-options, child lock, resume cleaning, power-saving, obstacle crossing mode, AI obstacle bitfield partial)
- Cleaning-schedule string format (CleanGenius preset + Custom-global; Custom per-room is structural only — see issue #1)
- Scale Inhibitor consumable (
siid 31) on top of brush/filter/sensor DreameDeviceOfflineErrordistinguished from other API errors- 11 of ~36
FEATURE_CONFIG_KEYSconfirmed by toggle (the rest documented by name only)
Known specifically-NOT-verified pieces
- Actions
START,PAUSE,STOP,CHARGE/dock,START_AUTO_EMPTY,START_WASHING, allRESET_*— wired with Tasshack's older-model siid:aiid values, but no live test SuctionLevel,WaterVolume,ChargingStatus,CleaningModeenum behaviour during actual cleaning (settings reads work; downstream effects untested)TASK_STATUS(siid 4 piid 1) — raw int only; values 3, 6, 13, 14, 17, 23 observed in different states without a clean mappingCleaningMode(siid 4 piid 23) — known to be a packed bitfield on r2532a (raw 5120 in baseline); not decoded- Per-room schedule packed-int format (issue #1)
- AI obstacle bitfield (
siid 4 piid 22) — partial decoding only; bits 1, 2, 4 verified, bits 0, 3, 5-8 unknown 0xC249middle bits of the Custom-mode global schedule int- The
siid 99 piid 98andsiid 6 piid 1compressed blobs (likely telemetry + live map) - AI object-detection class catalog (we see bbox class id 160 repeatedly; other class IDs not observed)
- Most of the
FEATURE_CONFIG_KEYS(~25 still documented by name only) - A real cleaning run — none triggered via the lib
Cloud-only settings (no MQTT push to the device)
Some app settings are not stored on the device at all — they live entirely in Dreame's cloud and never produce an MQTT echo. node-dreame can't observe or write these without a separate cloud-API endpoint:
- Auto-update toggle
- "Mopping with Detergent" master (note: distinct from "Mop-Washing with Detergent" which DOES push)
- Camera / Activation PIN
- Device rename (
customName) - Matter pairing / activation code
Matter support
The X50 Ultra Complete also supports Matter. node-dreame stays cloud-based; Matter would be a cleaner local-only path for basic robot vacuum capabilities (start/dock/battery), but exposes a much smaller surface than what's mapped here. See project_dreame_matter_future.md in the project memory for context if pivoting.
Reverse-engineering notes
For implementers (and for AI agents extending the spec), docs/ contains:
auth-flow.md— endpoint URLs, headers, request/response shapes for auth + device list + MQTTota-flow.md— observed timeline + envelope shapes from a real firmware update, including the OTApropschannel and thedowloadedtypospec-discovery-methodology.md— how the property/action catalogue was assembled from a live device, with all the verified mappings and what's still unknown
License
MIT — see LICENSE.
Acknowledgements
The MIoT property/action enum structure is informed by Tasshack's dreame-vacuum (Mi cloud). This library does not share code with it.
