Skip to content

eufy-mega / model/capabilities / 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.


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.


properties

ts
properties: PropertySpec[];

Properties this capability contributes.


reads?

ts
optional reads?: ReadSpec[];

Declarative typed read getters exposed on dev.<cap>() (e.g. dev.battery()?.level). The barrel installs each evidence-gated onto the action object — see ReadSpec and buildReads. A read-only capability (a sensor with no controls) declares reads and omits actions.

Methods

actions()?

ts
optional actions(
   ctx, 
   sink, 
   media?, 
   lockSettings?): 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).

lockSettings is a DELIBERATE, ACKNOWLEDGED exception to "this layer never names a capability" (unlike media, which is genuinely capability-neutral — shared by every media-capable device, not shaped around one). It exists 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 — inventing a properly capability-neutral query boundary for a single caller would be exactly the premature abstraction this codebase avoids elsewhere. Do not copy this pattern for the next read-style capability without re-deciding it. If that capability's read need is ALSO an ff09 lock-family concern, extend LockSettingsReader in place. If it's a genuinely different capability, that SECOND real use case is what justifies generalizing this into an actual capability-neutral primitive (mirroring how p2pQuery itself is already generic) — not a third capability-named parameter bolted on next to this one.

Parameters

ctx

CommandContext

sink

CommandSink

media?

MediaProvider

lockSettings?

LockSettingsReader

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.reads 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 or eufy. "eufy" and "Anker" are trademarks of their respective owners. Use responsibly — rapid or failed logins can trigger captcha or temporary cooldowns.