User Drivers
User Drivers allow you to create custom device drivers using JavaScript, extending GEM's built-in driver library to support proprietary or unusual equipment.
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
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:
- Click Add in the grid toolbar
- The driver editor opens with blank configuration
- Configure the basic settings (see Editor section below)
- Write your driver code
- Click Save to create the driver
- Click Reload to activate the new driver
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
- Variable: Virtual device for computed values or integrations
- Enabled - Toggle driver active state
Inheritance:
- Base Driver - Select which driver class to extend. The dropdown lists all available drivers. Common choices include:
generic_tcp- TCP/IP communicationgeneric_http- HTTP/REST APIsgeneric_serial- RS-232/RS-485generic_ir- Infrared controldevice_base- Raw device driver (no protocol)- Any built-in or user driver (e.g.,
lutron_base,controller_slave)
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
Code Editor & Debugger
The integrated code editor provides:
- Syntax Highlighting - JavaScript syntax coloring
- Line Numbers - Easy navigation
- Search and Replace -
Ctrl+Fto find,Ctrl+Hto replace - Code Formatting -
Ctrl+Shift+Bto beautify code - Status Bar - Shows cursor position (line, column)
- Unsaved Changes Warning - Warns before navigating away with unsaved edits
- Error Detection - Basic syntax checking
- 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.
- Debug - Click the Debug button to start a debug session. The current editor code is sent to the server (no save required).
- Step Controls - Step Over (
F10), Step Into (F11), Step Out (Shift+F11), Continue (F5). - Variables Panel - Shows all local variables at the current breakpoint. 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.
- Console - Shows
console.logoutput from the driver, timestamped with millisecond precision.
Action Buttons
- Cancel - Close editor without saving final changes
- Reload - Reload the driver into GEM's runtime (apply changes)
- Save - Save driver code and configuration
Reloading 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 clicking Reload, GEM lists every device using this driver and asks whether to cascade-reload them now. Choose OK to reload them all (each device disconnects and reconnects, so live drivers will briefly drop their connection); choose Cancel to leave them on the previous version until the next manual reload or restart.
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
connect() {
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
response(msg) {
return super.response(msg);
}
// Called when the device disconnects
disconnect() {
super.disconnect();
}
}
TCP Example
class my_tcp_driver extends generic_tcp {
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 this.send(cmd.template + this.delimiter);
return result;
}
response(data) {
console.log('received response:', data);
super.response(data);
}
async disconnect() {
console.log('disconnecting');
await super.disconnect();
}
}
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 use response sets
init()
- Called once when driver loads
- Set up polling
- Initialize state
- Register callbacks
Base Driver Classes
generic_tcp
For TCP/IP connected devices:
class my_tcp_driver extends generic_tcp {
constructor(device) {
super(device);
this.delimiter = '\r\n';
this.port = 23; // Default telnet port
}
async command(cmd) {
return await this.send(cmd.template + this.delimiter);
}
}
Methods Available:
send(data)- Send raw data over TCPwrite(string)- Write string to socketsetDelimiter(delim)- Set message delimiter
generic_http
For HTTP/REST APIs:
class my_api_driver extends generic_http {
async command(cmd) {
let url = this.getAttribute('api_url');
let key = this.getAttribute('api_key');
return await this.get(url + cmd.template, {
headers: {'X-API-Key': key}
});
}
}
Methods Available:
get(url, options)- HTTP GETpost(url, data, options)- HTTP POSTput(url, data, options)- HTTP PUTdelete(url, options)- HTTP DELETE
generic_serial
For RS-232/RS-485 devices:
class my_serial_driver extends generic_serial {
constructor(device) {
super(device);
this.baudRate = 9600;
this.parity = 'none';
this.dataBits = 8;
this.stopBits = 1;
}
async command(cmd) {
return await this.send(cmd.template + '\r');
}
}
Methods Available:
send(data)- Send data over serialsetPort(path)- Set serial port path
device_base
For custom protocols or direct driver implementation:
class my_custom_driver extends device_base {
async connect() {
// Implement custom connection logic
}
async disconnect() {
// Implement custom disconnection logic
}
async command(cmd) {
// Implement custom command sending
}
response(data) {
// Implement custom response parsing
}
}
Accessing Device Data
Attributes
// Get attribute value
let ipAddress = this.getAttribute('ip_address');
let port = this.getAttribute('port', 8080); // With default
// 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 this.send(cmd.template);
return result;
} catch (error) {
console.error('command failed:', error.message);
throw error; // Propagate to caller
}
}
Testing
Use the Test Device feature:
- Create a device using your driver
- Select it in the Test Device field
- Make code changes
- Click Reload button
- Test commands through the device's Commands tab
- 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.send('?STATUS\r');
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.send('INIT\r');
await this.delay(1000);
await this.send('VERSION\r');
}
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
-
Class Naming: Driver class name must exactly match the Name field
-
Error Handling: Always implement try/catch for network operations
-
Logging: Use descriptive log messages for debugging
-
Cleanup: Always clean up resources (intervals, buffers) in disconnect()
-
Async/Await: Use async/await for cleaner asynchronous code
-
Attributes: Store configuration in attributes, not hard-coded
-
Testing: Test thoroughly with a real device before production
-
Documentation: Comment complex logic
-
Version Control: Export driver code to external files for version control
-
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 this.send(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 this.send(template + '\r');
}
Stateful Communication
async command(cmd) {
// Some devices require login
if (!this.authenticated) {
await this.login();
}
// Now send command
return await this.send(cmd.template);
}
async login() {
let user = this.getAttribute('username');
let pass = this.getAttribute('password');
await this.send(`LOGIN ${user} ${pass}\r`);
this.authenticated = true;
}
Troubleshooting
Driver Not Loading
- Check Class Name: Must match the Name field exactly
- Check Syntax: Look for JavaScript syntax errors
- View Logs: Check system logs for load errors
- Verify Base Driver: Ensure base driver exists and is valid
Commands Not Working
- Test Connection: Verify device is reachable
- Check Logs: Review command and response logs
- Debug send(): Log raw data being sent
- Verify Protocol: Confirm device protocol matches implementation
Driver Crashes
- Add Error Handling: Wrap operations in try/catch
- Check Resources: Ensure cleanup in disconnect()
- Validate Inputs: Check for null/undefined values
- Review Logs: Look for stack traces
Example Drivers
Simple HTTP API
class simple_api extends generic_http {
async command(cmd) {
let baseUrl = this.getAttribute('api_url');
let endpoint = cmd.template;
let response = await this.get(baseUrl + endpoint);
if (response.status === 'success') {
await this.setZoneState(cmd.zone_id, cmd.name, response.value);
}
}
}
Serial Device with Polling
class serial_device extends generic_serial {
constructor(device) {
super(device);
this.baudRate = 9600;
}
init() {
setInterval(() => this.pollStatus(), 10000);
}
async pollStatus() {
let status = await this.send('?STATUS\r');
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]));
});
}
}
}