Skip to content

Under the hood — how the SDK fits together

A tour of the public shape of eufy-mega for host developers: the objects you construct, how they relate, and the lifecycle they move through. Everything here is the public API — the names you import from the package. For the exhaustive, generated-from-source reference, see the API reference.

The one entry point: EufyMega

You construct one EufyMega per account. It's the facade over everything else — login, the device list, the realtime channels, and the unified event stream.

ts
import { EufyMega } from "eufy-mega";

const eufy = new EufyMega({ email, password, region: "eu", store });

Its job is orchestration only. It exposes three groups of methods:

  • Loginlogin(), solveCaptcha(), submitVerifyCode() (see Getting started).
  • DevicesgetDevices(), getDevice(sn) → capability-driven Device objects.
  • Realtime — SDK-managed: login() auto-starts the event channels and P2P opens on demand (see Transports and Connectivity & battery), all feeding one event surface. disconnect() tears it back down.

The lifecycle

The normal flow through the facade, start to finish:

new EufyMega(opts)


login()  ──► captcha? ──► solveCaptcha(answer)
   │                          │
   │        2fa? ──► submitVerifyCode(code)

login()                      // on success, realtime starts automatically (push + MQTT + wired P2P)

   ├── on("motion" | "contactState" | "doorbellPress" | …)   // typed semantic events, flowing
   ├── getDevices()                                           // resolve the account's devices
   └── getDevice(sn) → dev.camera?.()?.live()                   // opens P2P on demand, idle-detaches


disconnect()                 // tear the channels down; the login session stays valid

A restored session (via a store) skips straight past login. Connectivity is SDK-managed: the event channels come up on login and a camera's P2P session opens only when a command / stream / doorbell ring needs it, then idle-detaches so battery cameras sleep. See Connectivity & battery.

Devices are capability-driven, not subclassed

getDevice(sn) returns a single concrete Device — there are no per-model subclasses. A device is entirely described by its resolved { codec, capabilities, properties }, so you ask what it can do rather than what it is:

ts
dev.has("ptz"); // capability present?
dev.getProperty("battery"); // current state (or undefined)
dev.getProperties(); // full snapshot

Each capability the device HAS surfaces a fluent, typed accessor returning an action object — dev.camera?.(), dev.ptz?.(), dev.light?.(), dev.lock?.(), … A device carries only the accessors for capabilities it has, and each returns undefined until the device is bound to a live client — hence the two optional links:

ts
import { PtzDirection } from "eufy-mega";

await dev.camera?.()?.on();
await dev.ptz?.()?.rotate(PtzDirection.left, 1.0);

Argument constants (PtzDirection, ArmingMode, Watermark, VideoQuality, …) are exported from the package, so the choices autocomplete and can't drift. Writes whose behavior isn't yet verified on a mega device throw a clear error rather than guessing — reads always work via getProperty.

One event surface, many sources

Motion, person, doorbell, contact, lock, battery — however they arrive (push, P2P, or a poll fallback), they land as one typed event stream on the facade:

ts
eufy.on("motion", (e) => e.deviceSn);
eufy.on("event", (e) => e.eventName); // the catch-all, tagged with the true event name

The event names and payloads are typed, so eufy.on("motion", …) autocompletes and types e. See Events for the full list, and Transports for which channel carries what.

Consuming media

Camera media hangs off a bound camera's action object and is built around one pull, many consumers — every live view, snapshot, and recording on a camera shares a single session:

ts
const cam = (await eufy.getDevice(sn)).camera();
const stream = await cam?.live(); // raw frames
const r = await cam?.openReadable?.(); // a node Readable
for await (const frag of cam!.recordFragments!()) {
  /* CMAF for HLS/MSE */
}

The media surface (live, openReadable, recordFragments, snapshot, snapshotLive, record) is the MediaProvider contract. See Consuming a live stream for the full walkthrough (egress choices, keyframe priming, power budgets).

How the pieces layer up

Internally the SDK is organised as strict layers with one dependency direction — you consume the top of the stack and never see the rest:

LayerWhat it is (public view)
facadeEufyMega — the one object you construct and drive.
modelDevice + the capability action objects behind dev.<cap>().
contractsThe boundary types a device speaks — Command, CommandSink, MediaProvider, LiveStreamHandle.
transportThe realtime channels (push / MQTT / P2P) the SDK brings up automatically. Internal — resolved by intent, not selected by name.

Where to go next

Independent and unofficial. Not affiliated with, endorsed by, or sponsored by Anker Innovations or eufy. "eufy" and "Anker" are trademarks of their respective owners. Use responsibly — rapid or failed logins can trigger captcha or temporary cooldowns.