Appearance
Driver Lifecycle
A driver goes through five phases: startup, registration, device discovery, command handling, and shutdown. This guide walks through each phase with concrete code examples from the SDK sample driver.
1. Startup
Construct the driver with your credentials and connect:
typescript
const driver = new HosDriver({
driverKey: 'SIMULATED',
instanceId: 'simulated-001',
wsUrl: process.env.HOS_WS_URL ?? 'ws://localhost:8080/driver',
name: 'Simulated Light Driver',
});
driver.connect();Option reference:
driverKey— Uppercase identifier for this driver type. Pattern:[A-Z0-9_], 2-64 characters. The SDK normalizes the value to uppercase before validation, so'simulated'and'SIMULATED'are equivalent.instanceId— Unique runtime identifier for this instance. Pattern:[a-zA-Z0-9:_-], 1-128 characters. Differentiates multiple instances of the same driver type running simultaneously (e.g.hue-bridge-01,hue-bridge-02).wsUrl— HOS driver WebSocket endpoint. Format:ws://host:port/driver. Default port is8080on path/driver. Set via theHOS_WS_URLenvironment variable in practice.name— Optional display name shown in the HOS admin UI. Maximum 128 characters.autoRegister: true(default) — The SDK automatically sendsdriver.registeron every connect and reconnect. Leave this at the default unless you need manual registration control.
driver.connect() opens the WebSocket connection to HOS. The 'connected' event fires when the socket opens, before registration completes.
2. Registration
With autoRegister: true, the SDK handles registration automatically. Listen for the 'registered' event to know when it is safe to send events:
typescript
driver.on('registered', () => {
console.log('[simulated] Registered with HOS server');
// Safe to call sendEvent() here
});What happens under the hood:
- On WebSocket open, the SDK sends:json
{ "method": "driver.register", "params": { "driverKey": "SIMULATED", "instanceId": "simulated-001", "protocolVersion": 1 } } - The server validates
driverKey,instanceId, andprotocolVersion. If validation fails, the server responds with{ ok: false, error: "..." }and an'error'event fires on the driver. - On success, the server responds with
{ ok: true, event: "REGISTERED", driverKey: "...", instanceId: "..." }. - The SDK sets its internal
registeredflag and emits the'registered'event.
Do not call sendEvent() before 'registered' fires. The WebSocket is open after 'connected', but the server rejects events from unregistered drivers.
3. Device Discovery
After registration, announce the devices this driver controls. Call sendEvent('DEVICE_DISCOVERED', ...) once per device:
typescript
driver.on('registered', () => {
driver.sendEvent('DEVICE_DISCOVERED', {
device_id: 'sim-light-001',
data: {
name: 'Simulated Light',
deviceType: 'light',
properties: {
commandCatalog: [
{ key: 'turn_on', label: 'Turn On' },
{ key: 'turn_off', label: 'Turn Off' },
],
},
},
});Field reference:
device_id— Unique identifier for this device within this driver. Choose a stable value that persists across restarts (e.g. a MAC address, serial number, or bridge ID).data.name— Human-readable display name shown in the HOS UI.data.deviceType— Device category. See Entity Type Reference for all valid values. Common values:'light','thermostat','tv','lock','vacuum'.data.properties.commandCatalog— Array of{ key, label }objects advertising available commands. The mobile app uses this list to render device controls.
After discovering devices, emit initial state immediately so HOS has a baseline:
typescript
driver.sendEvent('STATE_UPDATE', {
device_id: 'sim-light-001',
data: { power: false, brightness: 0 },
});
});Other discovery events:
sendEvent('DEVICE_UPDATED', ...)— Use this when device metadata changes (name, commandCatalog) without the device going away.sendEvent('DEVICE_REMOVED', ...)— Use this when a device permanently disconnects or is deleted. Passdevice_idin the payload.
4. Command Handling
HOS sends ACTION commands to the driver when a user triggers a device control. Use onAction() to register a handler:
typescript
driver.onAction((msg) => {
const action = msg.data.action;
const success = action === 'turn_on' || action === 'turn_off';
// Always send ACTION_RESULT first
driver.sendEvent('ACTION_RESULT', {
device_id: msg.device_id,
data: {
success,
requestId: msg.data.requestId as string | undefined,
},
});
// Then update state to reflect the new device condition
if (action === 'turn_on') {
driver.sendEvent('STATE_UPDATE', {
device_id: msg.device_id,
data: { power: true, brightness: 100 },
});
} else if (action === 'turn_off') {
driver.sendEvent('STATE_UPDATE', {
device_id: msg.device_id,
data: { power: false, brightness: 0 },
});
}
});Message fields:
msg.event— Always'ACTION'for command messages.msg.device_id— Identifies which device the command targets. Match this against your discovereddevice_idvalues.msg.data.action— The command key fromcommandCatalog(e.g.'turn_on','set_temperature').msg.data.requestId— Optional correlation ID. Echo it back inACTION_RESULTso HOS can match the response to the originating request.msg.data— May contain additional payload fields depending on the command (e.g.value: 72for aset_temperaturecommand).
Response pattern:
- Send
ACTION_RESULTwithsuccess: trueorsuccess: false(andrequestIdif present). - Send
STATE_UPDATEwith the new device state after the command executes on the physical device.
5. Graceful Shutdown
Handle OS signals to disconnect cleanly:
typescript
process.on('SIGTERM', () => {
console.log('[simulated] SIGTERM received, disconnecting...');
driver.disconnect();
});
process.on('SIGINT', () => {
console.log('[simulated] SIGINT received, disconnecting...');
driver.disconnect();
});driver.disconnect() does three things:
- Sets the
stoppingflag so automatic reconnect will not fire. - Cancels any pending reconnect timer.
- Closes the WebSocket with close code
1000("Driver disconnecting").
After disconnect() is called, the server marks the driver session as closed and stops routing commands to it. Always handle both SIGTERM (from process managers and container orchestrators) and SIGINT (Ctrl-C in the terminal).
Reconnection
HosDriver reconnects automatically when the WebSocket closes unexpectedly. Reconnect behavior is configurable but the defaults work for most cases:
| Option | Default | Description |
|---|---|---|
reconnect | true | Enable/disable automatic reconnect |
reconnectBaseMs | 1000 | Starting delay (ms) before first retry |
reconnectMaxMs | 30000 | Maximum delay cap (ms) |
The reconnect algorithm uses exponential backoff: delay doubles on each attempt, starting at reconnectBaseMs, capped at reconnectMaxMs. Each delay also has ±10% random jitter applied to prevent thundering-herd reconnect storms when many drivers restart simultaneously.
For example, with defaults:
- Attempt 1: ~1 second
- Attempt 2: ~2 seconds
- Attempt 3: ~4 seconds
- Attempt 4: ~8 seconds
- ...caps at ~30 seconds per attempt
When autoRegister: true (default), the driver automatically re-sends driver.register after every reconnect. The 'registered' event fires again and device discovery re-runs — this is intentional and expected behavior.
Exception: Close code 4001 means the server replaced this session because another connection arrived with the same driverKey + instanceId. HosDriver does not reconnect on 4001 — reconnecting would create an infinite loop. An 'error' event fires instead.
For common reconnect failures and how to debug them, see Troubleshooting.
What's Next
- Driver Architecture — How the full command flow works from mobile app through HOS to your driver and back
- Domain Reference — Standard device types, state fields, and commands per domain
- Entity Type Reference — Complete
deviceTypevalues, tag conventions, and property schemas