Skip to content

Events

Device events arrive from up to four transports (FCM push, P2P frames, cloud poll, secure MQTT) and are normalized into one typed semantic event each — you listen without caring which transport delivered it. Event names autocomplete and payloads are typed:

ts
eufy.on("motion", (e) => console.log(e.deviceSn, e.thumbnailUrl));
eufy.on("doorbellPress", (e) => …);
eufy.on("personDetected", (e) => …);
eufy.on("lockState", (e) => …);
eufy.on("contactState", (e) => e.open); // entry sensor: true = open (push or cloud poll)
eufy.on("batteryLevel", (e) => e.to); // new 0–100 level (cloud poll — see cadence below)
eufy.on("strangerDetected", (e) => …); // a person the device does NOT recognise
eufy.on("soundDetected", (e) => …); // also cryingDetected, vehicleDetected, dogDetected
eufy.on("armingModeChanged", (e) => …); // guard mode switched — re-read the mode
eufy.on("alarm", (e) => e.phase); // "triggered" | "delayed"
eufy.on("ptzNotify", (e) => e.kind); // "rotate" | "zoom" | "position"

// Catch-all: one listener for EVERY semantic event — payload tagged with `name` (a discriminated
// union, so `switch (e.eventName)` narrows the type). Ideal for fanning to a bus.
eufy.on("event", (e) => bus.emit(e.eventName, e));

Events start flowing on their own — a successful login() brings up the account-wide push channel (and MQTT for appliances) automatically, so you register listeners and receive events without any extra setup. Subscribe before logging in if you don't want to miss an early event. Which transport carries each event, and how P2P opens on demand, is covered in Realtime transports and Connectivity & battery.

Low-level escape hatches remain (message, p2p, pushRaw, connect/disconnect lifecycle, error) for when you want the raw frame.

Detection kinds are separate events, not one motion with a flag — a host usually maps them to distinct sensors. Note personDetected means a face or a recognised person; someone the device does not recognise arrives as strangerDetected.

Not every event arrives at the same speed. Push- and P2P-carried events (motion, doorbellPress, personDetected, lockState, contactState) land within seconds of the device acting. Anything the cloud reports instead of the device is bounded by the cloud's own refresh of that device's params — minutes, not seconds. Treat those as a slowly-updating level, not a trigger.

MQTT semantic events are pending. The mqtt source is wired end-to-end (messages already flow through the normalizer) but no capability maps it to a semantic event yet — the realtime state payload for vacuum / eufy_home appliances isn't decoded yet. Until then, consume MQTT via the raw message event; the semantic layer picks it up once a capability adds an mqtt mapping.

Example

ts
/**
 * Example 02 — subscribe to typed semantic events.
 *
 * Connects the realtime transports and listens for normalized events (motion, doorbell, person,
 * lock, contact, battery, PTZ). Event names autocomplete and payloads are typed.
 *
 *   EUFY_EMAIL=… EUFY_PASSWORD=… node examples/02-listen-events.ts
 *
 * Requires `npm run build` first.
 */
import { loginClient } from "./_client.ts";

async function main(): Promise<void> {
  const eufy = await loginClient();
  eufy.on("error", (e) => console.error("[error]", e.message));

  // Semantic events — same shape regardless of which transport delivered them.
  eufy.on("motion", (e) => console.log("motion", e.deviceSn, e.thumbnailUrl ?? ""));
  eufy.on("personDetected", (e) => console.log("person", e.deviceSn));
  eufy.on("doorbellPress", (e) => console.log("doorbell", e.deviceSn));
  eufy.on("lockState", (e) => console.log("lock", e.deviceSn));
  eufy.on("contactState", (e) => console.log("contact", e.deviceSn, e.to));
  eufy.on("ptzNotify", (e) => console.log("ptz", e.stationSn, e.kind));

  // Catch-all — one listener for every semantic event; `e.eventName` says which. Handy for fanning
  // events to a host's event bus without registering a listener per name.
  eufy.on("event", (e) => console.log("· any:", e.eventName, e.deviceSn ?? e.stationSn ?? ""));

  console.log("listening 60s — trigger something on a device…");
  await new Promise((r) => setTimeout(r, 60_000));
  await eufy.disconnect();
}

main().catch((e: unknown) => {
  console.error("FATAL", e instanceof Error ? e.message : String(e));
  process.exit(1);
});

Next: Realtime transports · Live media.

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.