Skip to main content

User Drivers

User Drivers allow you to create custom drivers using JavaScript, extending GEM's built-in driver library. A device driver adds support for proprietary or unusual equipment; a variable driver adds a new source of system variables — values macros, triggers, and schedules can read with [$name].

Licensing

User Drivers — and the Script Console — are part of the Custom Driver Authoring module. Creating a custom driver requires the module in your license; the nav item is locked otherwise. Trials and development builds unlock it, and existing user drivers keep working if a license later changes. See License.

Overview

The User Drivers page provides a complete development environment for creating, testing, and debugging custom drivers. Each driver extends a base driver class and can implement custom communication protocols, parsing logic, and state management.

Viewing User Drivers

The main grid displays all configured user drivers with the following columns:

  • ID - Unique identifier
  • Name - Driver class name
  • System Target - What the driver controls (device or variable)
  • Base Driver - Parent class being extended
  • Enabled - Whether the driver is currently active
  • Status - Whether the driver actually compiled and loaded

The Status column is worth watching. A driver that fails to compile is still a row in the table: the devices using it silently fall back to the base driver, stay "connected" and answer commands by doing nothing. Status shows compile error for those, with the reason on hover, so the failure is visible where the driver was written rather than only in the system log.

Grid Actions

  • Add - Create a new user driver
  • Edit - Open the driver editor
  • Delete - Remove a user driver
  • Reload - Refresh the grid data

Creating a User Driver

To create a new user driver:

  1. Click Add in the grid toolbar
  2. The driver editor opens with blank configuration
  3. Configure the basic settings (see Editor section below)
  4. Write your driver code
  5. Click Create Driver

Saving validates the code first (see Validation) and loads it into the running system — there is no separate activation step. If devices are already using the driver, GEM says which of them are still running the previous version and offers to reload them.

Driver Editor

The driver editor provides a comprehensive development environment:

Basic Configuration

Driver Information:

  • Name - Driver class name (must match your class name in code)
  • System Target - Choose "Device" or "Variable"
    • Device: Controls physical hardware. Assign it to a device on the Devices page.
    • Variable: Publishes named values on an interval. Configure instances of it on the Variables page, where it appears in the driver list marked user driver. See Writing Your Own Variable Driver for the shape of the class.
  • Enabled - Toggle driver active state. Clearing it (or deleting the driver) takes effect at the next load: the compiled class is dropped, so a device loading afterwards uses its base driver instead. Devices already running the class keep it until they are reloaded — the same rule as a code change.

Switching the System Target on a new driver swaps the starting template to match. A saved driver's code is never replaced.

Inheritance:

  • Base Driver - Select which driver class to extend. The dropdown lists the drivers available for the selected System Target. Common choices include:

    • generic_tcp - TCP/IP communication
    • generic_http - HTTP/REST APIs
    • generic_serial - RS-232/RS-485
    • generic_ir - Infrared control
    • device_base - Raw device driver (no protocol)
    • Any built-in or user driver (e.g., lutron_base, controller_slave)

    For a Variable target the list is variable_base (the default), the built-in variable drivers, and any other user driver with a variable target — extend one of those to reuse its behavior, or variable_base to start clean.

important
Your class always writes extends BaseDriver

BaseDriver is the only parent class name bound inside a driver, and it resolves to whatever this field names. Writing the concrete name — extends generic_tcp, extends GenericTCP — does not resolve and is refused on save.

Extending another user driver works the same way: pick it here and its compiled class becomes your BaseDriver, inheriting its methods as a built-in would. An inheritance loop (a extends b extends a) falls back to the generic base rather than recursing, and is logged.

Test Configuration

For Device Drivers:

  • Test Device - Select an existing device using this driver
  • Reload button - Reload the test device to apply driver changes

This allows you to:

  • Test driver changes immediately
  • Debug with real device communication
  • Iterate quickly during development

The Test Device is also what a debug session attaches to — base driver calls run against it and the Protocol Monitor shows its live traffic. See Sandbox and attached sessions.

Finding what uses this driver

Below the form, a saved driver offers Open Devices using this driver, which opens the Devices page already filtered to it — the plain list holds every device on the site. A Variable driver offers Open Variables the same way, scoped to that driver's own instances on the Variables page.

Code Editor & Debugger

The integrated code editor provides:

  • Syntax Highlighting - JavaScript syntax coloring
  • Line Numbers - Easy navigation
  • Search and Replace - Ctrl+F to find, Ctrl+H to replace
  • Code Formatting - Ctrl+Shift+B to reformat the driver
  • Status Bar - Cursor position (line, column) at the top right of the editor
  • Unsaved Changes Warning - Closing the editor with unsaved edits asks before discarding them
  • Error Detection - Syntax errors are marked in the gutter as you type, and the full check runs on save (see Validation)
  • Console Output - Runtime logging with timestamps
  • Maximize - Click the maximize button (top-right corner) to expand the editor to full screen

Debugger

The built-in debugger lets you step through driver code:

  • Breakpoints - Click in the gutter to set/remove breakpoints before or during a debug session. They work anywhere there is code — including inside a constructor and inside a getter.
  • Debug - Click the Debug button to start a debug session. The current editor code is sent to the server (no save required).
  • Run… - Choose which method the session calls after the driver is constructed, and pass it arguments as JSON. The default is connect() for a device driver and update() for a variable driver; naming response with ["PWR=ON"] is how you exercise parsing logic without any hardware.
  • Step Controls - Step Over (F10), Step Into (F11), Step Out (Shift+F11), Continue (F5), Pause (F6).
  • Pause - Stops a running driver wherever it has got to, with the frame intact. Use it when the console says the driver is running and has not stopped at a breakpoint: Pause shows you the line it is sitting on, which is usually a device or a query that has not answered.
  • Variables Panel - Shows the locals, closure values, parameters and this of the paused frame. Click a variable name to load it into the evaluation input. Click long values to expand/collapse them.
  • Hover Evaluation - While paused at a breakpoint, hover over any variable or expression in the editor to see its current value in a tooltip.
  • Watch Expressions - Add expressions to evaluate at each step.
  • Evaluation Bar - Enter arbitrary JavaScript expressions to evaluate in the current debug context, including this.
  • Console - Shows console.log output from the driver, timestamped with millisecond precision.
  • Protocol Monitor - The second tab in the bottom pane. See Protocol Monitor.
Where breakpoints work

Everywhere V8 can stop, which is everywhere there is code: inside a constructor, inside a getter, inside a callback, inside a loop body. The driver runs completely unmodified — GEM does not rewrite it to make it steppable — so the program you step through is the program that runs in production, the line numbers are your own, and a stack trace points at your file.

A breakpoint set on a line with no executable code (a blank line, a comment, a closing brace) moves to the next line that has some, and the console says so. That is the same behaviour as a browser's debugger.

Pausing on an error

An uncaught exception stops on the line that threw, with the frame still intact — locals, this and the call stack are all there to inspect, and you can evaluate expressions against them. Continue from there and the error is reported as it normally would be. This is usually faster than reading a stack trace, because the state that caused the throw has not been unwound yet.

Sandbox and attached sessions

Debug sessions run in an isolated worker thread, never on the controller's main loop, so a driver that never terminates cannot take the site offline: GEM notices the session has stopped responding and ends it, and everything else keeps running. GEM attaches a V8 inspector to that thread from the main thread, which is what allows real breakpoints in unmodified code — V8 stops the driver's thread, and the thread doing the debugging is a different one that never stops.

  • Sandbox (no Test Device selected) — the driver runs detached. gem calls reach the real server and the real database; base-driver calls run against a real instance of your Base Driver that is not bound to any device. So super.connect() works and returns, but nothing reaches the wire and no connection monitoring is started — there is no device to monitor. A transport that has no address does not connect, so this.command(…) answers {error: 'socket not ready'} rather than sending.
  • Own connection (a Test Device selected, mode Own connection) — the session takes that device's settings onto its own instance and opens its own connection. super.connect() really connects, await this.command(…) really sends, and the device's replies are delivered to the response() in the editor — so a breakpoint in response() stops on real device data, with the frame as a Buffer exactly as in production. The driver already serving the site is untouched. Two things to know: it is a second connection to that device, so hardware that allows only one session will refuse it; and the session stays open after connect() returns, because that is when the device starts talking. It closes — and closes its connection — on Stop or after the idle timeout.
  • Attached (a Test Device selected, mode Attached) — base-driver calls run against that live device, riding its existing socket, so this.ip, this.zones and this.commands are the real ones and sends reach the wire. Replies are not routed into the editor: they belong to the running driver. super.connect(), super.connectSocket() and super.disconnect() are refused in this mode and tell you so — the first two would open a second socket on the running device and orphan the first, and the last would take the device offline for the whole site. Use Own connection when you want those to run for real.

The badge in the debugger toolbar always shows which mode you are in, and the selector next to it chooses between the two device modes.

In every mode the base driver and gem live on the main thread and are called across a thread boundary, so await every base-driver and gem call. await on a plain value does nothing at runtime, which makes awaiting correct in production too. this.socket is not reachable from a debug session — send with await this.command(…), which is the queued write path the monitor and the trace hooks can see.

Validation

Saving runs the driver through the same compile the loader uses, and refuses to store code that cannot load. Errors are listed under the form and marked in the editor gutter on the line they came from. The checks are:

  • JavaScript syntax, reported at the line and column the parser stopped on
  • import / export — a user driver is evaluated as a plain script, not a module, so both are errors rather than style opinions
  • the class name must match the driver's Name exactly (the loader resolves the driver by that name; a mismatch is the classic "my driver does nothing" failure)
  • extends BaseDriverBaseDriver is bound to whatever the Base Driver field names, so naming a concrete class such as GenericTCP cannot resolve
  • the Base Driver must exist, and cannot be the driver itself
  • anything the definition-phase compile throws, such as an undefined global

Warnings (a class extending nothing, extra class declarations) are shown but do not block a save. If you are sure the validator is wrong, the error panel's Save anyway path is the escape hatch — the driver is stored exactly as written.

Code History

Every save that changes the code snapshots the previous version first, keeping the last 20 per driver. Click History in the editor to list them with their timestamp, the user who saved, and the size; View shows the code, Restore puts it back. A restore snapshots what it replaces, so restoring is itself undoable. Renaming a driver or toggling Enabled takes no snapshot — only code changes do, so the history window is never flushed by unrelated edits.

Protocol Monitor

The bottom pane's second tab shows what the selected Test Device actually exchanged with GEM — outbound frames (TX) and inbound frames (RX), in the order they happened, with millisecond timestamps. It taps the transport itself, so it shows the raw frame rather than the driver's interpretation of it: TCP frames appear exactly as written and read, UDP datagrams exactly as sent and received (inbound ones labelled with the sender's address), HTTP requests appear with their method, URL and status, and non-printable payloads are rendered as hex.

The last 400 frames per device are kept in memory (never persisted), and the pane streams new ones live. Pause freezes the view without losing frames; Clear empties the buffer.

Credentials do not reach the monitor: writes a driver makes directly to its socket (the login exchange on most protocols) bypass the traced path, and HTTP auth headers are never recorded.

Action Buttons

  • History - Previous versions of this driver's code (see Code History)
  • Cancel - Close the editor. If there are unsaved edits, GEM asks before discarding them.
  • Reload - Reload the stored driver into GEM's runtime without saving the editor buffer — use it to pick up a change made elsewhere
  • Save & Apply - Validate, snapshot, store and load the driver, then offer to reload the devices still running the previous version
note

Loading a user driver only refreshes the in-memory driver record — devices already running the previous version of the driver class keep running it until they themselves are reloaded. After a save or a reload, GEM lists every device using this driver and asks whether to reload them now. Reload them all disconnects and reconnects each device, so live drivers briefly drop their connection; Not now leaves them on the previous version until the next manual reload or restart.

Each device in that list is a link. Click it to open the device in a reference modal over the prompt — the editor, its unsaved code and the choice itself all stay behind it — so "is now the moment to disconnect this one?" can be answered before answering it.

Variable instances are the exception: restarting one swaps a timer rather than a connection, so every variable instance running the driver is restarted automatically on reload and the toast reports how many.

Driver Structure

User drivers are JavaScript classes that extend a base driver. When creating a new driver, the editor is pre-populated with a commented template showing the key methods to override.

Basic Template

class my_custom_driver extends BaseDriver {

// Called when the device connects
async connect() {
await super.connect();
}

// Called when a command is sent to the device
async command(cmd) {
return await super.command(cmd);
}

// Called when data is received from the device
async response(msg) {
return await super.response(msg);
}

// Called when the device disconnects
async disconnect() {
await super.disconnect();
}
}

What a driver can use

A driver is evaluated as a plain script in a sandboxed context — not a module — so the only names in scope are the ones GEM puts there:

  • BaseDriver — the class named by the Base Driver field
  • gem — the GemServer instance (gem.query, gem.setAttribute, gem.command, …)
  • console — output goes to the system log and to the editor's console pane
  • Buffer, setTimeout, setInterval, clearTimeout, clearInterval, setImmediate, queueMicrotask
  • fetch, URL, URLSearchParams, TextEncoder, TextDecoder, AbortController, structuredClone
  • the standard JavaScript built-ins — JSON, Math, Date, RegExp, Promise, parseInt, encodeURIComponent, and the rest

Nothing else exists: there is no require, no import, no process, no fs, and no npm package is reachable. For HTTP, use fetch — or, on a driver whose base is generic_http, this.httpRequest(url, opts), which goes through the driver's own auth, TLS and timeout handling and shows up in the Protocol Monitor.

The same names are in scope inside a debug session, so a driver that runs in one runs in the other.

TCP Example

Base Driver set to generic_tcp:

class my_tcp_driver extends BaseDriver {
constructor(device) {
super(device);
this.delimiter = '\r\n';
}

async connect() {
console.log('connecting to device:', this.device.name);
await super.connect();
}

async command(cmd) {
console.log('sending command:', cmd);
let result = await super.command(cmd.template);
return result;
}

async response(data) {
console.log('received response:', data);
return await super.response(data);
}

async disconnect() {
console.log('disconnecting');
await super.disconnect();
}
}

Sending Data

A driver puts bytes on the wire through command(), which is the queued write path — the protocol monitor and the driver trace hooks both watch it:

await this.command('PWR ON'); // string: request_terminator is appended
await this.command(Buffer.from([0x02, 0x41, 0x03])); // Buffer: sent byte for byte, unchanged
await this.command({name: 'power_on', args: {}}); // object: rendered from its command template

request_terminator defaults to \r, so do not append it yourself to a string command — you would send it twice. Write it into the Buffer form if you need exact control over the framing.

warning
Inside your own command() override, call super.command(…)

this.command(…) from within an overridden command() calls your method again and recurses until the stack gives out. Use this.command(…) from connect(), onConnect(), a poll or any other method; use super.command(…) inside command() itself.

Writing straight to this.socket works but bypasses the queue, the throttle and the monitor, and it is not available at all inside a debug session. Prefer this.command().

Key Methods to Override

connect()

  • Called when device connects
  • Initialize connection parameters
  • Establish communication
  • Call super.connect() for base class behavior

disconnect()

  • Called when device disconnects
  • Clean up resources
  • Close connections
  • Call super.disconnect() for cleanup

command(cmd)

  • Called when a command is executed
  • cmd contains: {name, template, args, zone_id, device_id}
  • Send command to device
  • Return result or throw error

response(data)

  • Called when data is received from device
  • Parse incoming data
  • Update zone states
  • Trigger events
  • Call super.response(data) to forward parsed responses for zone state processing

init()

  • Called once when driver loads
  • Set up polling
  • Initialize state
  • Register callbacks

Variable Drivers

A driver with its System Target set to Variable doesn't talk to a device — it publishes named values on an interval, the same way the built-in weather, sun/moon, and water-data drivers do. Once saved it appears in the driver list on the Variables page (marked user driver), where each row is one configured instance of it.

The shape is different from a device driver: there is no connect(), command(), or response(). Implement async update() and publish through this.getName(...).

class tide_height extends BaseDriver {

constructor(data) {
super(data);
// names (before the instance prefix) and value types this driver publishes
this.variables = { tide_height: 'float' };
}

// Optional — drives the driver list label, the default interval, the
// Configuration form, and the Produced Variables list on the Variables page
static getMetaData() {
return {
display_name: 'Tide Height',
description: 'Current tide height for a NOAA station',
status: 'stable',
default_interval: 900000,
config: {
template: { station_id: '' },
fields: { station_id: 'NOAA CO-OPS station id' }
},
variables: { tide_height: {type: 'float', description: 'height in feet'} }
};
}

async update() {
let cfg = this.data || {};
let height = await fetchHeight(cfg.station_id);
// getName() applies the instance prefix — always publish through it
await gem.setAttribute('variable', 0, this.getName('tide_height'), height, 'float');
}
}
  • update() runs once immediately when the instance loads, then on the instance's Update Interval. The base constructor starts that first run before the lines in your own constructor have executed, so read this.data rather than relying on fields you assign after super().
  • this.data is the instance's Configuration JSON from the Variables page; this.prefix is its name prefix. Publishing through this.getName(...) is what lets two instances coexist.
  • getMetaData() is optional — without it the driver still runs, and the Variables page falls back to the driver name with an empty Configuration form.
  • A built-in variable driver of the same name wins, so avoid weather, sun_moon, season, datetime, google_calendar, and usgs_waterdata.

Full reference, including how to read the values back with [$name], is in Variables.

Base Driver Classes

Pick one in the Base Driver field; the class itself always says extends BaseDriver, which is what that field binds.

generic_tcp

For TCP/IP connected devices:

class my_tcp_driver extends BaseDriver {
constructor(device) {
super(device);
this.delimiter = '\r\n';
this.port = 23; // Default telnet port
}

async command(cmd) {
return await super.command(cmd.template);
}
}

Methods Available:

  • send(data) - Send raw data over TCP
  • write(string) - Write string to socket
  • setDelimiter(delim) - Set message delimiter

generic_http

For HTTP/REST APIs:

class my_api_driver extends BaseDriver {
async command(cmd) {
let url = await this.getAttribute('api_url');
let key = await this.getAttribute('api_key');

let resp = await this.httpRequest(url + cmd.template, {
method: 'GET',
headers: {'X-API-Key': key}
});

return resp; // {status, data} — or {error} when the request failed
}
}

Methods Available:

  • httpRequest(url, options) - one request, resolving to {status, data} or {error}. Never rejects, so there is nothing to try/catch around it. options takes method, headers, data, auth, timeout, and lenientHttp: true for firmware that mis-frames chunked responses.
  • httpRequest(options) - same call with the URL inside the options object.

Requests made this way appear in the Protocol Monitor with their method, URL and status; auth headers are never recorded.

generic_serial

For RS-232/RS-485 devices:

class my_serial_driver extends BaseDriver {
constructor(device) {
super(device);
this.baudRate = 9600;
this.parity = 'none';
this.dataBits = 8;
this.stopBits = 1;
}

async command(cmd) {
return await super.command(cmd.template);
}
}

Methods Available:

  • send(data) - Send data over serial
  • setPort(path) - Set serial port path

device_base

For custom protocols or direct driver implementation:

class my_custom_driver extends BaseDriver {
async connect() {
// Implement custom connection logic
}

async disconnect() {
// Implement custom disconnection logic
}

async command(cmd) {
// Implement custom command sending
}

async response(data) {
// Implement custom response parsing
}
}

Accessing Device Data

Attributes

// Get attribute value — async, so always awaited
let ipAddress = await this.getAttribute('ip_address');
let port = await this.getAttribute('port') || 8080;

// Set attribute value
await this.setAttribute('power_state', 'on');
await this.setAttribute('volume', 50, 'int');

// Set attribute with options (object form)
await this.setAttribute({ name: 'arm_state', value: 'armed', value_type: 'string', history: true });

// Check if attribute exists
if (this.hasAttribute('api_key')) {
// Use api_key
}

Zones

// Get all zones for this device
let zones = this.zones;

// Find specific zone
let zone = zones.find(z => z.name === 'main_zone');

// Update zone state
await this.setZoneState(zone.id, 'power', 'on');
await this.setZoneState(zone.id, 'volume', 65);

// Get zone state
let power = await this.getZoneState(zone.id, 'power');

Device Properties

// Access device object
console.log('Device:', this.device.name);
console.log('Driver:', this.device.driver);
console.log('Address:', this.device.address);

// Check connection state
if (this.connected) {
// Device is connected
}

Logging and Debugging

Console Logging

// Standard logging
console.log('debug message');
console.warn('warning message');
console.error('error message');

// Structured logging
console.log('command sent:', {
command: cmd.name,
template: cmd.template,
zone: cmd.zone_id
});

Logs appear in:

  • Driver editor console
  • System logs (Insights > Logging)
  • Terminal output (development mode)

Error Handling

async command(cmd) {
try {
let result = await super.command(cmd.template);
return result;
} catch (error) {
console.error('command failed:', error.message);
throw error; // Propagate to caller
}
}

Testing

Use the Test Device feature:

  1. Create a device using your driver
  2. Select it in the Test Device field
  3. Make code changes
  4. Click Reload button
  5. Test commands through the device's Commands tab
  6. Review logs for debugging

Advanced Topics

Polling

Implement automatic status queries:

init() {
// Poll every 5 seconds
this.pollingInterval = setInterval(() => {
this.pollStatus();
}, 5000);
}

async pollStatus() {
try {
let status = await this.command('?STATUS');
this.parseStatus(status);
} catch (error) {
console.error('polling error:', error);
}
}

disconnect() {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
super.disconnect();
}

State Machines

Manage complex device states:

constructor(device) {
super(device);
this.state = 'disconnected';
}

async connect() {
this.state = 'connecting';
await super.connect();
this.state = 'initializing';
await this.initialize();
this.state = 'connected';
}

async initialize() {
// Send initialization commands
await this.command('INIT');
await this.delay(1000);
await this.command('VERSION');
}

Response Parsing

Complex response parsing:

response(data) {
// Handle multi-line responses
if (data.includes('BEGIN_STATUS')) {
this.statusBuffer += data;

if (data.includes('END_STATUS')) {
this.parseFullStatus(this.statusBuffer);
this.statusBuffer = '';
}
return;
}

// Parse single-line responses
let match = data.match(/VOL=(\d+)/);
if (match) {
let volume = parseInt(match[1]);
this.zones.forEach(zone => {
this.setZoneState(zone.id, 'volume', volume);
});
}
}

Event Handling

Respond to system events:

onZoneCommand(zone, command) {
console.log(`Zone ${zone.name} received command: ${command.name}`);
// Custom handling before command execution
}

onAttributeChange(name, value) {
if (name === 'ip_address') {
console.log('IP address changed, reconnecting...');
this.reconnect();
}
}

Best Practices

  1. Class Naming: Driver class name must exactly match the Name field

  2. Error Handling: Always implement try/catch for network operations

  3. Logging: Use descriptive log messages for debugging

  4. Cleanup: Always clean up resources (intervals, buffers) in disconnect()

  5. Async/Await: Use async/await for cleaner asynchronous code

  6. Attributes: Store configuration in attributes, not hard-coded

  7. Testing: Test thoroughly with a real device before production

  8. Documentation: Comment complex logic

  9. Version Control: Export driver code to external files for version control

  10. Security: Never log passwords or API keys

Common Patterns

Simple On/Off Control

async command(cmd) {
let commands = {
'power_on': 'PWR ON\r',
'power_off': 'PWR OFF\r'
};

if (commands[cmd.name]) {
return await super.command(commands[cmd.name]);
}
}

Parametric Commands

async command(cmd) {
let template = cmd.template;

// Replace variables
template = template.replace('{level}', cmd.args.level);
template = template.replace('{zone}', cmd.args.zone);

return await super.command(template);
}

Stateful Communication

async command(cmd) {
// Some devices require login
if (!this.authenticated) {
await this.login();
}

// Now send command
return await super.command(cmd.template);
}

async login() {
let user = this.getAttribute('username');
let pass = this.getAttribute('password');

await this.command(`LOGIN ${user} ${pass}`);
this.authenticated = true;
}

Troubleshooting

Driver Not Loading

  1. Read the Status column on the grid, or the badge beside the driver's name in the editor. compile error names the reason on hover — that is the whole answer most of the time
  2. Check Class Name: Must match the Name field exactly. A mismatch reports class <name> was not defined
  3. Check the parent: the class must say extends BaseDriver; naming a concrete driver does not resolve
  4. Check Syntax: Save runs the full validation and marks the failing line
  5. View Logs: Check system logs for load errors
  6. Verify Base Driver: Ensure base driver exists and is valid

Commands Not Working

  1. Test Connection: Verify device is reachable
  2. Check Logs: Review command and response logs
  3. Debug send(): Log raw data being sent
  4. Verify Protocol: Confirm device protocol matches implementation

Driver Crashes

  1. Add Error Handling: Wrap operations in try/catch
  2. Check Resources: Ensure cleanup in disconnect()
  3. Validate Inputs: Check for null/undefined values
  4. Review Logs: Look for stack traces

Example Drivers

Simple HTTP API

Base Driver generic_http:

class simple_api extends BaseDriver {
async command(cmd) {
let baseUrl = await this.getAttribute('api_url');
let endpoint = cmd.template;

let response = await this.httpRequest(baseUrl + endpoint);

if (response.data && response.data.status === 'success') {
await this.setZoneState(cmd.zone_id, cmd.name, response.data.value);
}
}
}

Serial Device with Polling

Base Driver generic_serial:

class serial_device extends BaseDriver {
constructor(device) {
super(device);
this.baudRate = 9600;
}

init() {
setInterval(() => this.pollStatus(), 10000);
}

async pollStatus() {
let status = await this.command('?STATUS');
this.parseStatus(status);
}

parseStatus(data) {
let match = data.match(/POWER:(\w+),VOLUME:(\d+)/);
if (match) {
this.zones.forEach(zone => {
this.setZoneState(zone.id, 'power', match[1].toLowerCase());
this.setZoneState(zone.id, 'volume', parseInt(match[2]));
});
}
}
}
  • Devices - Creating devices that use custom drivers
  • Commands - Defining commands for devices
  • Technical Reference - Full driver API documentation