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
optional description?: string;

Human description for docs.


detection?

ts
optional detection?: DetectionSpec;

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


emits?

ts
optional 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
optional 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
optional 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
optional 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
optional 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
optional 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
optional 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
optional 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
optional 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
optional 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
optional 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.