Skip to content

@mega-yfue/eufy-sdk / CapabilityModule

Interface: CapabilityModule ​

A capability module: property schema + detection + inbound decode + outbound commands. Written once, reused by every device that lists the capability.

Properties ​

capability ​

ts
capability: Capability;

description? ​

ts
description?: string;

Human description for docs.


detection? ​

ts
detection?: DetectionSpec;

How devices are detected as having this capability. Absent = only via codec baseline / registry.


emits? ​

ts
emits?: readonly string[];

Semantic event names this capability emits from decodeEvent, which the flat events table cannot express — a frame the module parses itself has no id to list there.

Declared so the published manifest can state what a capability emits without a caller subscribing blind: the manifest's event list is this union'd with the table's own emit names. A capability whose events all come from the table omits it; decode-event.spec.ts locks the union against a hardcoded list, since the typed event map erases at build and cannot keep this honest.


events? ​

ts
events?: EventMapping[];

Declarative inbound-event mappings (push eventType / poll param → semantic event name). The common case: pure data, no code. The barrel indexes these for direct lookup.


line? ​

ts
line?: ProductLine;

The product line this capability belongs to; defaults to security (the bulk of the catalogue).

Checked BEFORE any detection evidence, so a capability can never land on a device from another line. This matters because the detection fields are OR-ed and several capabilities are found by NAME alone — eufy's retail vocabulary collides across ecosystems, so a light called "Outdoor Spotlights" or a strip named for a water effect otherwise picks up a camera or leak capability whose wire it does not speak.


members? ​

ts
members?: Members;

The capability's surface, one entry per feature — the schema, the getters, the setters, the intent routes and the descriptions all derived from it. See ./members.ts.

A feature is spelled ONCE: properties is propertiesOf(X_MEMBERS), never a second list.

Absent only on a capability with no surface to bind: pure detection (video, snapshot), or one whose object is a projection rather than device params (info).


ownedByStation? ​

ts
ownedByStation?: boolean;

The capability belongs to whichever device OWNS the group, so a device that hangs off a parent station must not claim it — even when a curated row grants it or the device mirrors the param.

Guard mode is the case that grounds this: a standalone SoloCam/Indoor cam owns its own mode (read live: armingMode answers on a standalone T8170/T8171/T8410), but behind a HomeBase the hub owns it and the app shows it there, not per camera — an attached T8170 reports no mode at all. Applied AFTER the three tiers are unioned (see resolveDevice), because the point is to withhold a control the device cannot answer for however it was granted.


properties ​

ts
properties: PropertySpec[];

Properties this capability contributes.

A module with a members table sets this to propertiesOf(ITS_MEMBERS) rather than writing it out: the schema is derived from the same declaration as the getters and setters, and is materialised here so every existing consumer keeps reading it the same way.


stateEvents? ​

ts
stateEvents?: object[];

Semantic events that describe a state, and the payload field holding it — so a state that several transports report is announced once per real change instead of once per transport.

One physical change can reach the SDK on more than one path: an entry sensor's contact arrives as a station notify ~2 s before the same value arrives as an FCM push. Both are real, but they describe one change. Listing the event here makes the emitter edge-triggered on that field: a value equal to the last one announced for that device is suppressed.

Declare this ONLY for events carrying a settled state. An event that is a pulse — motion, a doorbell press — must be omitted: consecutive pulses are identical by nature and deduping them would drop real detections.

Edge-triggering is applied to realtime sources only. A poll still re-announces an unchanged state, so a missed realtime frame is re-synchronised rather than left waiting for the state to change again.

event ​

ts
event: string;

field ​

ts
field: string;

Methods ​

actions()? ​

ts
actions?(deps): CapabilityActions;

The object of bound action methods exposed on a device that HAS this capability (e.g. device.light() → {on, off, setBrightness}). Control actions resolve a Command and emit it through sink; media actions (snapshot/live/record) delegate to the optional media provider (absent on a model object not bound to a live client).

Every injected provider here names a technical job, never the capability that happens to be its first caller: media is shared by every media-capable device, and ff09Settings is named for the frame family it reads, so any device driven by that frame can use it. That is the rule, not a convention — a provider that cannot be named without saying "lock" or "vacuum" is a provider split in the wrong place.

ff09Settings exists as its own boundary because GET_SETTINGS is a request/reply query (decrypt + parse + pick P2P-vs-MQTT), and neither CommandSink (write-only, Promise<void>) nor MediaProvider.p2pQuery (P2P-only, raw passthrough, no decrypt) fits it. A further ff09 setting needing a live read is one more method on Ff09SettingsReader; a genuinely different wire family is its own provider, not another parameter bolted on next to this one.

Parameters ​

deps ​

MemberDeps

Returns ​

CapabilityActions


buildCommand()? ​

ts
buildCommand?(
   action, 
   value, 
   ctx
): Command | undefined;

Resolve a semantic action into a transport-neutral Command for THIS device, or undefined if this module doesn't handle (action). action is a capability-local verb (e.g. "on", "off", "setBrightness", "rotate"), NOT a param id. Uses ctx to pick the right variant. This is where per-device variance lives — once, in the module.

Parameters ​

action ​

string

value ​

string | number | boolean

ctx ​

CommandContext

Returns ​

Command | undefined


decodeEvent()? ​

ts
decodeEvent?(signal): CapabilityEvent | null;

Escape hatch for inbound signals a flat EventMapping table can't express — e.g. parsing a binary P2P frame (the pan-tilt position stream). Return the semantic CapabilityEvent or null. Simple capabilities use only events and omit this.

Parameters ​

signal ​

InboundSignal

Returns ​

CapabilityEvent | null


decodeState()? ​

ts
decodeState?(signal): DecodedState | null;

Recover device STATE from an inbound signal, as wire ids in this device's own param namespace — the input to Device.applyParams, so a realtime-only line's values reach the same CapabilityModule.members getters a pollable cloud param would.

Separate from CapabilityModule.decodeEvent because the two answer different questions: a report that repeats the current state carries no event worth emitting but must still refresh the readable state. A module that decodes both shares one private parser between the hooks.

Parameters ​

signal ​

InboundSignal

Returns ​

DecodedState | null


realtimeInit()? ​

ts
realtimeInit?(ctx): Command[];

Commands to send once this device's realtime channel is up — e.g. a state-snapshot request, for a line whose state is pushed on change with no periodic heartbeat, so the typed reads are populated at connect instead of only after the first write. Best-effort: a failure is reported, never fatal.

Parameters ​

ctx ​

CommandContext

Returns ​

Command[]

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