API Reference
For developers integrating with GEM or extending it. GEM exposes several distinct surfaces; this section documents each.
Socket event names, REST endpoints, driver method signatures, macro step data keys, and the gem.* / context.* macro-script surface are frozen. New ones are added; existing ones are never repurposed or removed without a deprecation period. Build against them with confidence.
Surfaces
REST
GET /api/data/[entity] to read configuration and live state, POST /api/control/[action] to issue commands and run macros. Read-only for data — write methods return 501. Token-authenticated, gated by the same RBAC actions as the socket surface.
WebSocket
Socket.io is the primary real-time channel: state subscriptions, commands, and most interactive operations. Every socket event is auto-enumerated for RBAC, so adding a server handler automatically makes it a permissionable action.
Web Services
/web_service/[service] — custom HTTP endpoints backed by a script you write, for webhooks and external integrations.
Macro Scripts
The gem.* and context.* objects available inside the Run Script macro step. This is a public API: signature changes silently break stored automations, so it is treated as frozen.
Driver Development
How to write a device driver: extend device_base.js (or a generic transport), implement connect() / command() / response() / disconnect(), and expose metadata and commands. Includes relay state translation, per-zone action overrides, and toggle emulation.
Command payloads
Every surface dispatches through the same server entry point and takes the same payload — REST POST /api/control/command, the command socket event, and gem.command(...) in a macro script are the same contract:
- Verb —
command. There is noactionkey. - Target —
zone_id/device_idby id, orzone/deviceby name. - Parameters —
args.
await gem.command({ zone_id: 12, command: 'on' });
await gem.command({ zone_id: 12, command: 'level', args: { level: 75 } });
await gem.command({ zone: 'front_gate', command: 'open' });
await gem.command({ device_id: 3, command: 'power_on' });
Zone shorthand
A zone payload that names no command is read as shorthand: the one remaining key becomes the command, and its value is folded into args under that same name.
await gem.command({ zone_id: 12, level: 75 });
// expands to { command: 'level', zone_id: 12, args: { level: 75 } }
The expansion runs once at the very front of dispatch, before the zone or device is resolved — so it is a client-facing convenience only. A driver never sees the shorthand; by the time the payload reaches one it is always the canonical {command, args} form, and drivers read their inputs from args.
The rules are narrow:
- Only when there is no
commandkey. An explicitcommandalways wins, and a top-level value sitting beside it is then simply ignored — nothing downstream reads it, so that value belongs inargs. - Only for zone targets. A
device_id-only payload is never shorthand; give it an explicitcommand. - Exactly one key may remain besides
zone_id/zone/argsand any_-prefixed engine keys. A second one turns the expansion off and the payload goes through with no command at all. - An existing
argsis preserved, with the shorthand key merged over it. - An id or a name both work.
{zone_id: 5, level: 60}and{zone: 'kitchen', level: 60}each resolve.
Common client API
The frontend GemApp class wraps the WebSocket surface. A representative slice:
const gem = GemApp.getInstance();
// Data
await gem.query('zone', { subsystem_id: 11 });
await gem.insertModel('model', data);
await gem.updateModel('model', id, updates);
// Attributes — getAttributeValue for the value, getAttribute for the whole row
await gem.getAttributeValue('device', deviceId, 'ip_address'); // the cast value, undefined if unset
await gem.getAttribute('device', deviceId, 'ip_address'); // the row; {} when missing, so TRUTHY
await gem.setAttribute('zone', zoneId, 'brightness', 50, 'integer');
// Commands
await gem.command({ zone_id: zoneId, command: 'on' });
await gem.command({ zone_id: zoneId, command: 'level', args: { level: 100 } });
// Subscriptions
const token = await gem.subscribe('zone', zoneId, callback);
await gem.unsubscribe('zone', zoneId, token);