HTTP API Reference

The HomeCmdr API is a JSON HTTP + WebSocket server exposed by the api binary.

Default base URL: http://127.0.0.1:3000

The bind address is configured via api.bind_address in config/default.toml.


Authentication

All routes except GET /health and GET /ready require a Bearer token:

Authorization: Bearer <token>

Roles: read, write, admin, automation

RoleSatisfies
readRead endpoints (all GETs + WebSocket)
writeMutation endpoints
adminDiagnostics, reload, key management
automationAdmin + Automation

The master key (configured via auth.master_key or HOMECMDR_MASTER_KEY) always grants admin. API keys are created and scoped through the /auth/keys endpoints.

See Authentication for full setup details.


Rate Limiting

Write endpoints are subject to an optional token-bucket rate limiter. When a request exceeds the configured rate:

HTTP 429 Too Many Requests

Configured in config/default.toml:

[api.rate_limit]
enabled = false
requests_per_second = 100
burst_size = 20

Rate limiting is disabled by default.


Conventions

  • Request and response bodies are JSON.
  • Device IDs are stable namespaced strings: elgato_lights:light:0, roku_tv:tv.
  • Room IDs are user-defined strings: living_room, outside.
  • Commands use canonical capability/action pairs (see Command Shape).

Health

GET /health

Returns a simple liveness response. No authentication required.

curl http://127.0.0.1:3000/health
{ "status": "ok" }

GET /diagnostics/reload_watch

Returns current reload-watch configuration for scenes, automations, and scripts.

{
  "status": "ok",
  "watches": [
    { "target": "scenes", "enabled": true, "directory": "config/scenes" },
    { "target": "automations", "enabled": false, "directory": "config/automations" },
    { "target": "scripts", "enabled": false, "directory": "config/scripts" }
  ]
}

Adapters

GET /adapters

Returns all configured adapters that were successfully built and enabled.

curl http://127.0.0.1:3000/adapters \
  -H 'Authorization: Bearer <token>'
[
  { "name": "open_meteo", "status": "running" },
  { "name": "roku_tv", "status": "running" }
]

Devices

GET /devices

Returns all devices in the in-memory registry (including devices restored from persistence).

Optional query param ids filters to specific device IDs in the requested order:

curl "http://127.0.0.1:3000/devices?ids=open_meteo:temperature_outdoor&ids=open_meteo:wind_speed"

Unmatched IDs are silently omitted.

GET /devices/{id}

Returns one device by ID.

curl http://127.0.0.1:3000/devices/roku_tv:tv

Returns 404 with { "error": "device 'nonexistent' not found" } if not found.

POST /devices/{id}/room

Assigns a device to a room or clears its room assignment.

{ "room_id": "living_room" }

Clear assignment:

{ "room_id": null }

Returns 404 if the device does not exist. Returns 400 if the referenced room does not exist.

POST /devices/{id}/command

Sends one canonical command to one device.

curl -X POST http://127.0.0.1:3000/devices/roku_tv:tv/command \
  -H 'Content-Type: application/json' \
  -d '{"capability":"power","action":"toggle"}'

With a value:

curl -X POST http://127.0.0.1:3000/devices/elgato_lights:light:0/command \
  -H 'Content-Type: application/json' \
  -d '{"capability":"brightness","action":"set","value":50}'

Success:

{ "status": "ok" }

Error responses:

CodeMeaning
404Device does not exist
400Invalid command or adapter rejected it
501Commands not implemented for this device

Rooms

GET /rooms

Returns all rooms.

POST /rooms

Creates or updates a room.

{ "id": "living_room", "name": "Living Room" }

Both id and name must be non-empty.

GET /rooms/{id}

Returns one room by ID.

GET /rooms/{id}/devices

Returns all devices assigned to a room.

curl http://127.0.0.1:3000/rooms/living_room/devices

POST /rooms/{id}/command

Fans out one canonical command to every device in the room.

curl -X POST http://127.0.0.1:3000/rooms/living_room/command \
  -H 'Content-Type: application/json' \
  -d '{"capability":"power","action":"off"}'
[
  { "device_id": "roku_tv:tv", "status": "ok", "message": null },
  { "device_id": "open_meteo:wind_speed", "status": "unsupported", "message": "device commands are not implemented" }
]

Per-device statuses: ok, unsupported, error.


Groups

Groups are explicit user-defined collections of devices. Membership is static and managed directly.

GET /groups

Returns all groups.

[
  {
    "id": "bedroom_lamps",
    "name": "Bedroom Lamps",
    "members": ["zigbee2mqtt:bedside_left", "zigbee2mqtt:bedside_right"]
  }
]

POST /groups

Creates or updates a group.

{
  "id": "bedroom_lamps",
  "name": "Bedroom Lamps",
  "members": ["zigbee2mqtt:bedside_left", "zigbee2mqtt:bedside_right"]
}

Validation: non-empty id and name; all members must refer to existing devices.

GET /groups/{id}

Returns one group by ID.

DELETE /groups/{id}

Deletes a group.

POST /groups/{id}/members

Replaces group membership with an explicit device list.

{ "members": ["zigbee2mqtt:bedside_left", "zigbee2mqtt:bedside_right"] }

Returns 400 if any member device does not exist. Duplicate IDs are deduplicated (first-seen order preserved).

GET /groups/{id}/devices

Returns all devices currently in the group.

POST /groups/{id}/command

Fans out one canonical command to every device in the group.

curl -X POST http://127.0.0.1:3000/groups/bedroom_lamps/command \
  -H 'Content-Type: application/json' \
  -d '{"capability":"power","action":"off"}'

Response shape matches POST /rooms/{id}/command.


Scenes

GET /scenes

Returns all loaded scenes.

[
  { "id": "video", "name": "Video", "description": "Prepare devices for a video call" }
]

POST /scenes/reload

Reloads the scene catalog from disk. Validates all files first; atomically swaps on success; keeps the previous catalog if any file fails.

{
  "status": "ok",
  "target": "scenes",
  "loaded_count": 12,
  "errors": [],
  "duration_ms": 9
}

WebSocket events emitted: scene.catalog_reload_started, scene.catalog_reloaded, scene.catalog_reload_failed.

POST /scenes/{id}/execute

Executes one scene by ID.

curl -X POST http://127.0.0.1:3000/scenes/video/execute
{
  "status": "ok",
  "results": [
    { "target": "roku_tv:tv", "status": "ok", "message": null },
    { "target": "elgato_lights:light:0", "status": "ok", "message": null }
  ]
}
CodeMeaning
200Completed
202Queued (scene uses queued mode)
404Scene not found
423Dropped (scene uses single mode and is already running)

Automations

POST /automations/reload

Reloads the automation catalog. Validates, atomically swaps, restarts trigger loops, preserves enabled/disabled toggles for unchanged IDs.

Response shape is the same as POST /scenes/reload with target: "automations".

WebSocket events emitted: automation.catalog_reload_started, automation.catalog_reloaded, automation.catalog_reload_failed.


Scripts

POST /scripts/reload

Acknowledges script directory changes and emits lifecycle events. Does not interrupt in-flight scene or automation executions.

Response shape is the same as other reload endpoints with target: "scripts".


Capabilities

GET /capabilities

Returns the canonical capability catalog used for device and command validation.

{
  "capabilities": [
    {
      "domain": "lighting",
      "key": "brightness",
      "schema": { "type": "percentage", "values": [] },
      "read_only": false,
      "actions": ["set", "increase", "decrease"],
      "description": "Brightness level as a percentage (0-100)."
    }
  ],
  "ownership": { ... }
}

Current domains include: weather, lighting, sensor, climate, energy, media, access-control.


Authentication Keys

POST /auth/keys

Creates a new API key.

GET /auth/keys

Lists all API keys (hashed; plaintext is only shown at creation time).

DELETE /auth/keys/{id}

Revokes an API key.


WebSocket Events

GET /events

Upgrade to a WebSocket connection to receive live runtime events.

wscat -c ws://127.0.0.1:3000/events -H 'Authorization: Bearer <token>'

Current event types:

Event typeDescription
device.state_changedDevice attribute state updated
device.removedDevice removed from registry
device.room_changedDevice assigned to or removed from a room
room.addedRoom created
room.updatedRoom name changed
room.removedRoom deleted
group.addedGroup created
group.updatedGroup name changed
group.removedGroup deleted
group.members_changedGroup membership updated
adapter.startedAdapter completed startup
system.errorRuntime error from an adapter or internal component

Example frames:

{ "type": "adapter.started", "adapter": "roku_tv" }
{ "type": "device.state_changed", "id": "roku_tv:tv", "state": { "power": "off", "state": "online" } }
{ "type": "device.room_changed", "id": "roku_tv:tv", "room_id": "living_room" }
{ "type": "system.error", "message": "roku_tv poll failed: ..." }

Notes:

  • Internal DeviceSeen refreshes are filtered out of the public stream.
  • If a subscriber lags badly, a system.error frame is emitted indicating dropped events.

Device Shape

Important fields on every device object:

FieldDescription
idStable namespaced string, e.g. elgato_lights:light:0
room_idAssigned room, or null
kindSensor, Light, Switch, or Virtual
attributesCanonical state keyed by capability name
metadataNon-canonical adapter data including vendor_specific
updated_atChanges only when meaningful state changes
last_seenUpdates on every successful observation

Canonical Command Shape

{
  "capability": "...",
  "action": "...",
  "value": null
}

value is omitted for on, off, and toggle. It is required for set.

Examples:

{ "capability": "power", "action": "on" }
{ "capability": "brightness", "action": "set", "value": 42 }
{
  "capability": "color_temperature",
  "action": "set",
  "value": { "value": 3000, "unit": "kelvin" }
}

Agent / MCP Usage Notes

When integrating MCP tools or automation agents against a running HomeCmdr instance:

  • Use GET /scenes to discover available scene assets.
  • Use GET /devices to discover the live canonical device graph.
  • Use GET /rooms to discover the room model.
  • Use POST /scenes/{id}/execute for scene-driven orchestration.
  • Use POST /devices/{id}/room to attach devices to rooms.
  • Use POST /devices/{id}/command for direct device control.
  • Use POST /rooms/{id}/command for room-wide fan-out.
  • Connect to /events to react to runtime changes.

Treat the HTTP API as the external system contract and adapter crates as the integration contract.