Skip to content

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 is 8080 on path /driver. Set via the HOS_WS_URL environment variable in practice.
  • name — Optional display name shown in the HOS admin UI. Maximum 128 characters.
  • autoRegister: true (default) — The SDK automatically sends driver.register on 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:

  1. On WebSocket open, the SDK sends:
    json
    {
        "method": "driver.register",
        "params": {
            "driverKey": "SIMULATED",
            "instanceId": "simulated-001",
            "protocolVersion": 1
        }
    }
  2. The server validates driverKey, instanceId, and protocolVersion. If validation fails, the server responds with { ok: false, error: "..." } and an 'error' event fires on the driver.
  3. On success, the server responds with { ok: true, event: "REGISTERED", driverKey: "...", instanceId: "..." }.
  4. The SDK sets its internal registered flag 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. Pass device_id in 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 discovered device_id values.
  • msg.data.action — The command key from commandCatalog (e.g. 'turn_on', 'set_temperature').
  • msg.data.requestId — Optional correlation ID. Echo it back in ACTION_RESULT so HOS can match the response to the originating request.
  • msg.data — May contain additional payload fields depending on the command (e.g. value: 72 for a set_temperature command).

Response pattern:

  1. Send ACTION_RESULT with success: true or success: false (and requestId if present).
  2. Send STATE_UPDATE with 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:

  1. Sets the stopping flag so automatic reconnect will not fire.
  2. Cancels any pending reconnect timer.
  3. 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:

OptionDefaultDescription
reconnecttrueEnable/disable automatic reconnect
reconnectBaseMs1000Starting delay (ms) before first retry
reconnectMaxMs30000Maximum 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