Skip to main content

Driver Development

A driver teaches GEM to talk to one type of equipment. GEM ships 100+ drivers; this page is the contract for writing another.

Base classes

Extend device_base.js, or one of the generic transports when your device speaks a common protocol:

  • generic_tcp.js — TCP sockets
  • generic_http.js — HTTP/REST
  • generic_serial.js — serial
  • generic_ir.js — IR

The generic transports handle connection management so your driver focuses on the protocol.

The lifecycle methods

MethodResponsibility
connect()Establish the connection to the device.
command(cmd)Translate a GEM command (on, off, open, set_level, …) into the device's native protocol and send it.
response(msg)Parse incoming device data and turn it into attribute updates.
disconnect()Tear the connection down cleanly.

Declaring commands and metadata

  • getCommands() — returns the command table (templates, args, arg options). This table is authoritative: on each boot GEM's ensureCommands reconciles system command rows to match it, so the driver — not hand edits — owns command definitions. Integrators add new commands; they don't edit driver-managed ones.
  • getMetaData() — returns driver hints consumed by the admin UI: device-type hints, a command-set hint (enabling zero-touch command-set creation), required-attribute schema, and a sample configuration. Always report status: 'stable'.

Patterns to use, not reinvent

  • Relay state — never hardcode contact meaning. Route through relayToZoneState(relayState, zone) and zoneToRelayCommand(cmd, zone). See Relay Semantics.
  • Per-zone action overrides — exotic wiring (e.g. a shade on a pulse relay) can be retrofitted via a zone's <verb>_action attribute; the server intercepts and re-dispatches. Implement the verb natively when you can; overrides are the escape hatch.
  • Toggle emulation — for a state toggle the hardware lacks (e.g. mute_toggle), declare it in getCommands() for discoverability and let the server synthesize it from cached state, or short-circuit it in your own command().

State access

Read current values from the in-memory collections, not the database:

let device = GemServer.getInstance().devices[deviceId];
let val = device.some_attr;

See Architecture for why.

:::warning Don't write zone.source zone.source is GEM's av_source foreign key, owned by AV-routing macros — a driver writing it triggers "invalid av source for change." A physical input number belongs in zone.input, never zone.source. :::