@mega-yfue/eufy-sdk / EufyMega
Class: EufyMega
The package entry point — one client per Anker eufy account. Handles the login state machine (captcha/2FA/persistence), resolves the account's devices (getDevices/getDevice → capability-driven Devices), auto-manages the realtime channels (FCM push + secure MQTT start on login; P2P opens on demand per station and idle-detaches battery cameras), and fans every transport's traffic into one typed semantic event stream (eufy.on("motion", …)). Construct it with an EufyMegaOptions (email/password + optional session/push stores), then drive login() to completion — no connect* call needed (set autoRealtime:false to opt out). Call disconnect to tear the realtime channels down.
Extends
EventEmitter
Constructors
Constructor
new EufyMega(opts): EufyMega;Parameters
opts
Returns
EufyMega
Accessors
api
Get Signature
get api(): MegaHttpClient;Raw mega HTTP client, for endpoints not yet wrapped.
Returns
MegaHttpClient
loggedIn
Get Signature
get loggedIn(): boolean;True if a usable session (restored from store or freshly logged in) is held.
Returns
boolean
pollIntervalMs
Get Signature
get pollIntervalMs(): number;The effective cloud poll interval in ms — the configured EufyMegaOptions.pollMs or the default.
Returns
number
Methods
clearSession()
clearSession(): void;Forget the persisted session (forces a fresh login + 2FA next time) and clear account-owned media.
Returns
void
connectStation()
connectStation(deviceSn, signal?): Promise<void>;Open the P2P session for ONE device's station, and nothing else.
Auto-realtime warms every wired station on the account, which is what a host driving a fleet wants. A caller that needs exactly one station does not: an unreachable station broadcasts a local lookup to 255.255.255.255 once a second for the full connect timeout and sends a PPCS lookup to every cloud address in the same tick, so warming a fleet to talk to one camera is a burst of broadcast and NAT churn on the user's network for stations nobody asked about. Pair this with autoRealtime: false to open only what is being used.
Resolves when the station is connected; rejects on its connect timeout. Best-effort and idempotent — an already-open session resolves immediately.
Parameters
deviceSn
string
signal?
AbortSignal
Returns
Promise<void>
deviceAvailability()
deviceAvailability(sn):
| AvailabilityObservation
| undefined;Return the latest explicit availability observation for sn, or undefined when no verified vendor signal has been observed. This never derives a state from DeviceState.lastSeenMs, connection silence, P2P lifecycle, operation failures or caller-selected timeouts.
Parameters
sn
string
Returns
| AvailabilityObservation | undefined
deviceState()
deviceState(sn): DeviceState;The liveness facts for one device — see DeviceState. Facts, not an online verdict: "how long is too long" is a threshold that belongs to the caller, and it differs per device class.
The deviceState event announces when a device reports in.
Parameters
sn
string
Returns
disconnect()
disconnect(): Promise<void>;Tear down every realtime channel: close the secure-MQTT transport, all P2P sessions, and the FCM push socket. Idempotent — safe to call when nothing is connected. Leaves the login session intact (call login again to reconnect without re-authenticating).
Returns
Promise<void>
emit()
emit<E>(event, ...args): boolean;Type Parameters
E
E extends | "error" | "p2p" | "connect" | "disconnect" | "message" | "push" | "map" | "event" | keyof DeviceEventMap | "deviceAdded" | "deviceRemoved" | "deviceCapabilities" | "mapFrame" | "deviceState" | "availability" | "p2pConnect" | "p2pClose" | "p2pLevel2Ready" | "pushConnect" | "pushDisconnect" | "pushRaw" | "commandAck" | "commandUnconfirmed" | "sessionExpired"
Parameters
event
E
args
...EufyMegaEventMap[E]
Returns
boolean
getCleanRecords()
getCleanRecords(
deviceSn,
pageSize?,
page?
): Promise<CleanRecordPage>;One page of a robot vacuum's cleaning history, in whatever order the cloud returns it — newest first in practice, but that is the gateway's contract and the SDK does not re-sort.
pageSize is how many records to return and page is 1-based; page through until the returned total is reached. Answers an empty page rather than throwing when the account has no history for the device or the response cannot be read.
Each record carries a downloadUrl for the run's binary detail blob (map and per-run statistics). The SDK hands that URL over rather than fetching it — the host is unconfirmed and the blob's format is not evidenced yet.
Parameters
deviceSn
string
pageSize?
number = 20
page?
number = 1
Returns
Promise<CleanRecordPage>
getDevice()
getDevice(sn): Promise<Device>;Build a live Device model object for one serial: the resolved codec/capabilities with its current param values applied (named via the param dictionary; unknown ids kept as unknown_<pt>). This is the device primitive — dev.getProperties(), dev.has(cap), etc. Prefers fresh get_device_param_list, falls back to the device-list params. Under auto-realtime the returned Device is wired with a read-through freshness cache (see Device.setFreshnessPolicy), so repeat reads are served from cache instead of re-fetching, and realtime updates keep values fresh.
That refresh ANNOUNCES what it lands, like the other two inbound paths. Under frequent reads it fires every cacheTtlMs where the poll fires every ten minutes, so it is where most fresh cloud values arrive — and each announcing path is edge-triggered on the same live state, so whichever sees a change first announces it and the others stay silent. Its timing says only when a caller happened to read; the value is the news. It applies what the device volunteered over realtime on top of the cloud half, which the registry keeps apart, so it can neither revert nor announce a revert of a report already landed.
The Device returned is held WEAKLY: it is what the inbound paths announce against, so a caller that wants property changes for a serial keeps its own reference. Dropping it stops the announcements, not the device.
Parameters
sn
string
Returns
Promise<Device>
Example
const dev = await eufy.getDevice(sn);
if (dev.has("camera")) await dev.camera?.()?.snapshotStored?.();
console.log(dev.getProperty("battery"));getDevices()
getDevices(): Promise<EufyDevice[]>;List + classify devices across all houses (mega API). Each device is tagged with its API backend
- realtime transport. Camera/HomeBase records still appear here for inventory; driving them is P2P. Delegates to
DeviceRegistry(the house-scoped merge/dedupe lives there).
Side-effect: registers eufy_home_tuya devices with the Tuya command router so the command dispatcher can resolve a eufy SN → Tuya devId without a separate lookup. The Tuya id is the first non-empty tuya_uuid, tuya_virtual_id, tuya_device_id or virtualId on the raw cloud record, else the device's serial: the app keys its Tuya device by the eufy device id, and a record that carries none of those fields is read under its serial.
A partial cloud outage still resolves, with the devices that answered plus the ones already known — but a session the cloud has rejected REJECTS, with SessionExpiredError. An empty list would be indistinguishable from an account with no devices.
Returns
Promise<EufyDevice[]>
getMqttDevices()
getMqttDevices(): EufyDevice[];Devices that this client drives over MQTT (transport ≠ p2p).
Returns
getP2pSessions()
getP2pSessions(): Map<string, P2PSession>;The open P2P sessions, by key. P2P is auto-managed: wired stations are warmed at login, battery stations open on demand (command / stream, or an opted-in event pre-warm) and idle-detach — so this map grows and shrinks over time.
A station's own session is keyed by its serial, and p2pConnect(stationSn) / p2pClose(stationSn) track those. A station serving more than one camera at once also holds a session per extra camera, keyed <stationSn>#live:<channel> — these carry media alone and raise no connection events, because a station announces its state to every client that connects and reporting each copy would double every event the station's own session already delivers.
Returns
Map<string, P2PSession>
getStationFaces()
getStationFaces(stationSn, opts?): Promise<StationFace[]>;The people enrolled on a station, read from its own person_basic_info table over P2P.
The cloud roster (api.getFaces()) answers empty for an account that keeps its faces on the station, because they were never uploaded — the station holds them.
Resolves with the rows the station returned. Rejects on a station that cannot be reached, on a record stating no admin_user_id, and on the reply not arriving whole in time. Assembling that reply is P2PSession.readDatabase's; this resolves which station and which account id.
stranger<n> names are the station's own placeholders for a face nobody has named, and are returned as they arrive; a caller that wants only named people filters them.
Parameters
stationSn
string
opts?
StationFacesOptions = {}
Returns
Promise<StationFace[]>
getUserMqttInfo()
getUserMqttInfo(appName?): Promise<SecureMqttCredentials>;Fetch the per-user secure-MQTT credentials for realtime appliance control. Pass appName to request a specific capability scope on the current session without re-logging in — security devices (locks/garage) need the eufy_security scope, which the default scope can't reach.
Parameters
appName?
string
Returns
Promise<SecureMqttCredentials>
inspectDevice()
inspectDevice(sn): Promise<DeviceInspection>;Inspect one device by serial: resolve its codec/capabilities, cross-reference every reported param_type against the param dictionary, and emit a paste-ready registry.ts row plus dictionary snippets for anything unknown. Loads the device list if needed; prefers the live get_device_param_list for freshest params, falling back to the device-list params.
Parameters
sn
string
Returns
Promise<DeviceInspection>
keepStationWarm()
keepStationWarm(stationSn): () => void;Keep a T9000 station's RTC command session open and warm, so a command fires without a cold reconnect. Answers the hold's disposer; throws for any other station, and while logged out.
Parameters
stationSn
string
Returns
() => void
login()
login(opts?): Promise<LoginResult>;Begin (or resume) login. Returns a LoginResult — switch on status:
ok→ authenticated (result.session).captcha→ showresult.image, then solveCaptcha(answer).2fa→ a code was sent; submitVerifyCode(code).
A restored session resolves straight to ok. No exceptions for the expected captcha/2FA flow.
Parameters
opts?
messageType?
number
Returns
Promise<LoginResult>
Example
const res = await eufy.login();
if (res.status === "captcha") await eufy.solveCaptcha(await promptUser(res.image));
else if (res.status === "2fa") await eufy.submitVerifyCode(await promptUser());logout()
logout(): Promise<void>;Tear down realtime, clear passive media, and forget the persisted login session.
Returns
Promise<void>
off()
off<E>(event, listener): this;Type Parameters
E
E extends | "error" | "p2p" | "connect" | "disconnect" | "message" | "push" | "map" | "event" | keyof DeviceEventMap | "deviceAdded" | "deviceRemoved" | "deviceCapabilities" | "mapFrame" | "deviceState" | "availability" | "p2pConnect" | "p2pClose" | "p2pLevel2Ready" | "pushConnect" | "pushDisconnect" | "pushRaw" | "commandAck" | "commandUnconfirmed" | "sessionExpired"
Parameters
event
E
listener
(...args) => void
Returns
this
on()
on<E>(event, listener): this;Type Parameters
E
E extends | "error" | "p2p" | "connect" | "disconnect" | "message" | "push" | "map" | "event" | keyof DeviceEventMap | "deviceAdded" | "deviceRemoved" | "deviceCapabilities" | "mapFrame" | "deviceState" | "availability" | "p2pConnect" | "p2pClose" | "p2pLevel2Ready" | "pushConnect" | "pushDisconnect" | "pushRaw" | "commandAck" | "commandUnconfirmed" | "sessionExpired"
Parameters
event
E
listener
(...args) => void
Returns
this
once()
once<E>(event, listener): this;Type Parameters
E
E extends | "error" | "p2p" | "connect" | "disconnect" | "message" | "push" | "map" | "event" | keyof DeviceEventMap | "deviceAdded" | "deviceRemoved" | "deviceCapabilities" | "mapFrame" | "deviceState" | "availability" | "p2pConnect" | "p2pClose" | "p2pLevel2Ready" | "pushConnect" | "pushDisconnect" | "pushRaw" | "commandAck" | "commandUnconfirmed" | "sessionExpired"
Parameters
event
E
listener
(...args) => void
Returns
this
reboot()
reboot(sn): Promise<void>;Restart a station: a HomeBase, or a camera-family device that is its own station.
The restart is RESTART_HUB on the station's broadcast channel, so it reaches whatever owns the P2P session. A standalone camera or doorbell owns its own session and is the station it restarts. A camera attached to a HomeBase is not — the same frame would restart its HomeBase — so its serial throws, as does any non-camera serial, rather than doing nothing. The device drops its connection and returns after a minute or two; a HomeBase takes everything behind it offline meanwhile. Verified on real hardware against a HomeBase and a standalone indoor camera.
Parameters
sn
string
Returns
Promise<void>
reportedRtspUrl()
reportedRtspUrl(sn): Promise<string | undefined>;The device's LIVE, authoritative rtsp:// URL — host, path, and the credentials it is enforcing right now — or undefined when none is pushed within the read window.
A thin public door onto the P2P transport (which stays internal otherwise): opens the station's session on demand, so a viewer adopting a tile can call this directly without one already existing. It writes only the publish switch and the test-stream provoke, never the credentials, so a stream a NAS/NVR already consumes keeps its own pair.
This is the CANONICAL way to fetch the URL: it provokes and returns it. The rtsp capability's url member surfaces the SAME value as inbound state for code that already holds a dev.rtsp() and reacts to propertyChanged — not a second way to fetch it.
Every failure — no route, no account id, level-2 not ready, no push before the deadline — collapses to undefined. The distinction the caller might want (terminal "no RTSP" vs a transient "session not warm yet") is not drawn here yet; a caller that retries on undefined recovers from the transient case. The read window is a fixed 12 s — long enough for a cold HomeBase to wake and answer, and about the ceiling a UI adopting a tile will wait — deliberately not caller-tunable.
Parameters
sn
string
Returns
Promise<string | undefined>
setPollInterval()
setPollInterval(ms): void;Change the cloud poll interval at runtime; ms is the gap between polls, 0 disables polling.
Takes effect immediately: the pending tick is cancelled and the loop re-armed at the new interval (or left cancelled for 0). Unlike the constructor EufyMegaOptions.pollMs, this can be changed after login.
Parameters
ms
number
Returns
void
setProperty()
setProperty(
sn,
name,
value
): Promise<void>;Write a device property. Asks the capability modules to build the command for this (name, value) — the module owns how THIS device applies it. No (name, value) recipe → the device doesn't support the property, so we throw CapabilityNotSupportedError rather than a silent no-op. On success the device echoes the new state back as a param update — read it with getDevice to confirm.
Parameters
sn
string
device serial.
name
string
property name (e.g. "light", "brightness", "enabled").
value
string | number | boolean
desired value.
Returns
Promise<void>
Example
await eufy.setProperty(sn, "brightness", 50);
await eufy.setProperty(sn, "light", true);solveCaptcha()
solveCaptcha(answer, opts?): Promise<LoginResult>;Continue a {status:"captcha"} login with the solved answer. See login.
Parameters
answer
string
opts?
messageType?
number
Returns
Promise<LoginResult>
submitVerifyCode()
submitVerifyCode(code): Promise<LoginResult>;Continue a {status:"2fa"} login with the verify code that was sent. See login.
Parameters
code
string
Returns
Promise<LoginResult>
waitForRealtime()
waitForRealtime(options?): Promise<RealtimeReadiness>;Wait for the auto-managed realtime startup begun by the current successful login.
A caller-specific timeout does not cancel startup. Calls made before successful login reject with login() first; clients configured with autoRealtime:false resolve as disabled without opening a transport.
Parameters
options?
Returns
Promise<RealtimeReadiness>