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
capability: Capability;description?
optional description?: string;Human description for docs.
detection?
optional detection?: DetectionSpec;How devices are detected as having this capability. Absent = only via codec baseline / registry.
events?
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?
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
properties: PropertySpec[];Properties this capability contributes.
reads?
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()?
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
sink
media?
lockSettings?
Returns
buildCommand()?
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
Returns
| Command | undefined
decodeEvent()?
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
Returns
CapabilityEvent | null
decodeState()?
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
Returns
DecodedState | null
realtimeInit()?
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
Returns
Command[]