Skip to main content

Devices

A device in HomeCore is any physical or virtual entity that has a state you can read and optionally command. Devices are registered by plugins via MQTT and stored in the device registry (data/state.redb).

Device model​

{
"device_id": "yolink_d88b4c01000e82eb",
"canonical_name": "entryway.front_door",
"name": "Front Door",
"plugin_id": "plugin.yolink",
"area": "entryway",
"device_type": "contact_sensor",
"available": true,
"last_seen": "2026-03-28T14:22:00Z",
"attributes": {
"open": false,
"battery_pct": 85,
"signal": -65
}
}
FieldDescription
device_idUnique internal identifier; set by the plugin at registration
canonical_nameStable HomeCore name used for rules and logs when available
nameHuman-readable name; set by plugin, editable via API
plugin_idWhich plugin owns this device
areaRoom/zone assignment (optional)
device_typeCanonical category such as light, door_sensor, thermostat, or media_player
availableWhether the device is online
last_seenLast MQTT state update
attributesAll current attribute values as a flat JSON object

device_id vs canonical_name​

HomeCore now distinguishes between three kinds of names:

  • device_id: the plugin-owned internal ID used on MQTT topics and for runtime execution
  • canonical_name: the stable HomeCore name intended for rules, API consumers, and logs
  • name: the display label shown to humans

Example:

{
"device_id": "hue_001788fffe6841b3_1",
"canonical_name": "living_room.floor_lamp",
"name": "Floor Lamp"
}

In normal rule authoring, prefer canonical_name. Keep device_id for low-level API work, debugging, or when you need the exact plugin identifier.

Why this exists​

Display names are not always unique. You may have:

  • Floor Lamp in the living room
  • Floor Lamp in the bedroom
  • two separate Floor Lamp devices in the same room

canonical_name gives HomeCore a stable automation reference that remains readable without forcing rules to hardcode plugin IDs.

Device CRUD​

List devices​

# All devices
curl -s http://localhost:8080/api/v1/devices \
-H "Authorization: Bearer $TOKEN" | jq

# Paginated (large deployments)
curl -s "http://localhost:8080/api/v1/devices?limit=50&offset=0" \
-H "Authorization: Bearer $TOKEN" | jq
# X-Total-Count header gives total

# Filter by device type
curl -s "http://localhost:8080/api/v1/devices?device_type=media_player" \
-H "Authorization: Bearer $TOKEN" | jq '.[].device_id'

# Include resolved capability schema inline
curl -s "http://localhost:8080/api/v1/devices?device_type=media_player&include_schema=true" \
-H "Authorization: Bearer $TOKEN" | jq '.[].name'

Get one device​

curl -s http://localhost:8080/api/v1/devices/yolink_front_door \
-H "Authorization: Bearer $TOKEN" | jq

Get device capability schema​

curl -s http://localhost:8080/api/v1/devices/hue_001788fffe6841b3_1/schema \
-H "Authorization: Bearer $TOKEN" | jq

A schema is what lets a client render a device it has never heard of. It has three parts.

attributes — what the device reports and, where writable is true, what it accepts. Each carries the kind that picks a control, plus unit, min, max, step and options where they apply. Two fields say more than the value alone:

  • states names both sides of a boolean in the device's own words — open/closed, locked/unlocked, motion/clear — with the verb a trigger reads ("when the door opens"). A boolean is two events, not one, and without this a client shows "open, but Not" for the other half.
  • category says what a reading is for when it is not what the device is for: diagnostic for battery, signal, firmware, ip and model, config for a setting. Absent means primary. A door lock reports whether it is locked and also its battery; a client that renders both the same way buries the one you came for.

primary — the readings the device is for, most important first. A temperature/humidity sensor leads with temperature; a multi-sensor that reports motion leads with motion. homeCore derives this from the device's own type unless the plugin declared its own order, so a client with one row to fill does not have to guess.

actions — commands that are not attribute writes: a scene's activate, a shade's raise/lower/stop, a keypad's press_button. Each carries a label, an optional icon and its parameters, so a rule editor can offer them without knowing what brand it is talking to.

{
"primary": ["temperature", "humidity"],
"attributes": {
"temperature": { "kind": "float", "writable": false, "unit": "°C" },
"humidity": { "kind": "float", "writable": false, "unit": "%" },
"battery_pct": { "kind": "float", "writable": false, "unit": "%",
"category": "diagnostic" }
}
}

A device with no schema returns 404 with {"error": "schema not found"}. That is not a fault: a plugin declares what it knows, and some devices predate the descriptor. An empty attribute set is a different statement — a momentary output such as a pulsed contact closure genuinely has nothing to read, and says so.

Command a device​

PATCH /devices/{id}/state sends a command to the device via MQTT.

# Turn a light on
curl -s -X PATCH http://localhost:8080/api/v1/devices/hue_001788fffe6841b3_1/state \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"on": true, "brightness_pct": 80}'

# Unlock a door
curl -s -X PATCH http://localhost:8080/api/v1/devices/zwave_lock_front/state \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"locked": false}'

The command is published to homecore/devices/{id}/cmd. The plugin receives it and publishes the updated state back.

For some device types, especially media_player, the command payload is action-oriented rather than simple attribute assignment. Examples:

# Start playback on a media player
curl -s -X PATCH http://localhost:8080/api/v1/devices/sonos_living_room/state \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"action": "play"}'

# Play a Sonos favorite by name
curl -s -X PATCH http://localhost:8080/api/v1/devices/sonos_living_room/state \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"action": "play_favorite", "favorite": "Dinner Jazz"}'

# Use the generic media command shape
curl -s -X PATCH http://localhost:8080/api/v1/devices/sonos_living_room/state \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"action": "play_media", "media_type": "playlist", "name": "Dinner"}'

This is deliberate. The device ID stays stable inside HomeCore while the plugin handles the Sonos-specific lookup and transport details internally.

Update device metadata​

Change the display name, area, or canonical rule name without touching state:

curl -s -X PATCH http://localhost:8080/api/v1/devices/yolink_front_door \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Front Door Sensor", "area": "entryway", "canonical_name": "entryway.front_door"}'

Canonical naming guidance​

Good canonical names are:

  • lowercase
  • stable over time
  • area-qualified when useful
  • descriptive enough to avoid collisions

Examples:

  • living_room.floor_lamp
  • bedroom.floor_lamp
  • living_room.floor_lamp_sofa

Avoid using temporary or positional labels that are likely to change.

Delete a device​

Cascades to all rules that reference the device — affected rules are disabled with a DELETED: placeholder.

curl -s -X DELETE http://localhost:8080/api/v1/devices/old_sensor \
-H "Authorization: Bearer $TOKEN" | jq
# → {"deleted": true, "affected_rules": ["Morning lights", "Away mode check"]}

Bulk operations​

Bulk area assignment​

curl -s -X PATCH http://localhost:8080/api/v1/devices \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"ids": ["sensor_01", "sensor_02", "light_01"], "area": "garage"}' | jq
# → {"updated": 3, "not_found": []}

Bulk delete​

curl -s -X DELETE http://localhost:8080/api/v1/devices \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"ids": ["old_sensor_01", "old_sensor_02"]}' | jq
# → {"deleted": 2, "not_found": [], "affected_rules": ["Garage lights"]}

Wipe all devices for one plugin​

Removes every device whose plugin_id matches, with rule cascade. The plugin itself stays registered — devices it still cares about are re-registered automatically on its next sync cycle. Useful for cleaning up zombies left over from plugin development churn or config rearrangements without dropping the whole state DB.

curl -s -X DELETE http://localhost:8080/api/v1/plugins/plugin.hue/devices \
-H "Authorization: Bearer $TOKEN" | jq
# → {
# "deleted": 47,
# "device_ids": ["hue_light_1", "hue_scene_2", ...],
# "affected_rules": ["Morning routine"]
# }

This is an API-only operation today — the web UI does not surface a button for it. It requires the devices:write scope.

Device history​

Time-series attribute changes stored in SQLite (data/history.db):

# Last 24 hours (default)
curl -s "http://localhost:8080/api/v1/devices/thermostat_main/history" \
-H "Authorization: Bearer $TOKEN" | jq

# Specific attribute, last 7 days
curl -s "http://localhost:8080/api/v1/devices/thermostat_main/history?attribute=temperature&from=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
-H "Authorization: Bearer $TOKEN" | jq

# Response shape
# [{"attribute":"temperature","value":72.1,"recorded_at":"2026-03-28T14:00:00Z"}, ...]
ParameterDefaultDescription
from24 hours agoISO-8601 UTC start
tonowISO-8601 UTC end
attributeallFilter to one attribute
limit500Max entries (cap 5000)

Areas​

Assign devices to logical rooms/zones:

# List areas
curl -s http://localhost:8080/api/v1/areas \
-H "Authorization: Bearer $TOKEN" | jq

# Create an area
curl -s -X POST http://localhost:8080/api/v1/areas \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Living Room"}' | jq

# Assign devices to an area
curl -s -X PUT http://localhost:8080/api/v1/areas/AREA_ID/devices \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"device_ids": ["light_living_room_1", "light_living_room_2"]}' | jq

Device IDs by plugin​

PluginDevice ID patternExample
hc-huehue_{bridge_id}_{resource_id}hue_001788fffe6841b3_1
hc-yolinkyolink_{device_id}yolink_d88b4c01000e82eb
hc-lutronlutron_{device_id}lutron_21
hc-lutron (Pico)lutron_pico_{id}lutron_pico_5
hc-zwavezwave_{node_id}zwave_23
hc-sonossonos_{room_name}sonos_living_room
hc-wledwled_{name}wled_deck
hc-isyisy_{address}isy_1_2_3_1
Timerstimer_{name}timer_garage_close
Switchesswitch_{name}switch_away_mode
Modesmode_{name}mode_night
Lutron sceneslutron_scene_{id}lutron_scene_42
Hue sceneshue_{bridge}_scene_{id}hue_001788fffe6841b3_scene_abc123

Media players as first-class devices​

HomeCore now understands media_player as a built-in device type. A plugin can discover players and register them without manual configuration, and those players immediately become available in:

  • GET /api/v1/devices
  • GET /api/v1/devices?device_type=media_player
  • rules using set_device_state
  • scenes and any UI built on the device registry

For Sonos specifically, the player device can expose:

  • transport state such as playing, paused, and current position
  • volume and mute state
  • currently playing title, artist, and album
  • group topology
  • discovered Sonos favorites and playlists

That lets HomeCore rules refer to a logical device such as sonos_living_room instead of embedding http://speaker-ip:port/... URLs in automation logic.