Integrate with Home Assistant REST and WebSocket APIs. Use when making API calls, managing entity states, calling services, subscribing to events, or setting up authentication. Activates on keywords REST API, WebSocket, API endpoint, service call, access token, Bearer token, subscribe_events.
From cce-homeassistantnpx claudepluginhub nodnarbnitram/claude-code-extensions --plugin cce-homeassistantThis skill uses the workspace's default tool permissions.
README.mdDesigns and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Implements structured self-debugging workflow for AI agent failures: capture errors, diagnose patterns like loops or context overflow, apply contained recoveries, and generate introspection reports.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Master Home Assistant's REST and WebSocket APIs for external integration, state management, and real-time communication.
This skill prevents 5 common API integration errors and saves ~30% token overhead.
| Aspect | Details |
|---|---|
| Common Errors Prevented | 5+ (auth, WebSocket lifecycle, state format, error handling) |
| Token Savings | ~30% vs. manual API discovery |
| Setup Time | 2-5 minutes vs. 15-20 minutes manual |
# In Home Assistant UI:
# Settings → My Home → Create Long-Lived Access Token
# Store token securely (never commit to git)
export HA_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
export HA_URL="http://192.168.1.100:8123"
Why this matters: All API requests require Bearer token authentication. Creating a dedicated token for external apps allows you to revoke access without changing your password.
# Get all entity states
curl -X GET "${HA_URL}/api/states" \
-H "Authorization: Bearer ${HA_TOKEN}" \
-H "Content-Type: application/json"
Why this matters: Verifies your Home Assistant instance is accessible and your token is valid before building complex integrations.
Why this matters: Different use cases require different APIs. WebSocket excels at real-time apps; REST is simpler for occasional requests.
❌ Wrong - Missing Bearer prefix:
curl -X GET "http://ha:8123/api/states" \
-H "Authorization: ${HA_TOKEN}" # Missing "Bearer "
✅ Correct - Bearer prefix required:
curl -X GET "http://ha:8123/api/states" \
-H "Authorization: Bearer ${HA_TOKEN}"
Why: Home Assistant's API uses standard Bearer token authentication. The "Bearer " prefix tells the server this is a token-based auth scheme, not a username/password.
Get all states:
GET /api/states
Authorization: Bearer {token}
Response (200 OK):
[
{
"entity_id": "light.living_room",
"state": "on",
"attributes": {
"brightness": 255,
"color_mode": "color_temp",
"friendly_name": "Living Room Light"
},
"last_changed": "2025-12-31T18:00:00+00:00",
"last_updated": "2025-12-31T18:05:00+00:00"
}
]
Get single entity state:
GET /api/states/{entity_id}
Authorization: Bearer {token}
Create/update entity state:
POST /api/states/{entity_id}
Authorization: Bearer {token}
Content-Type: application/json
{
"state": "on",
"attributes": {
"friendly_name": "Custom Entity",
"custom_attribute": "value"
}
}
Response (201 Created or 200 OK):
{
"entity_id": "sensor.custom_sensor",
"state": "on",
"attributes": { ... }
}
Get all available services:
GET /api/services
Authorization: Bearer {token}
Response (200 OK):
[
{
"domain": "light",
"services": {
"turn_on": {
"description": "Turn on light(s)",
"fields": {
"entity_id": {
"description": "The entity_id of the light(s)",
"example": ["light.living_room", "light.bedroom"]
},
"brightness": {
"description": "Brightness 0-255",
"example": 180
}
}
},
"turn_off": { ... }
}
}
]
Call a service:
POST /api/services/{domain}/{service}
Authorization: Bearer {token}
Content-Type: application/json
{
"entity_id": "light.living_room",
"brightness": 180,
"transition": 2
}
Response (200 OK):
[
{
"entity_id": "light.living_room",
"state": "on",
"attributes": { ... }
}
]
Get all events:
GET /api/events
Authorization: Bearer {token}
Fire an event:
POST /api/events/{event_type}
Authorization: Bearer {token}
Content-Type: application/json
{
"custom_data": "value"
}
Get entity history:
GET /api/history/period/{timestamp}?filter_entity_id={entity_id}
Authorization: Bearer {token}
Response (200 OK):
[
[
{
"entity_id": "sensor.temperature",
"state": "22.5",
"attributes": { ... },
"last_changed": "2025-12-31T12:00:00+00:00"
}
]
]
Get Home Assistant configuration:
GET /api/config
Authorization: Bearer {token}
Response (200 OK):
{
"latitude": 52.3,
"longitude": 4.9,
"elevation": 0,
"unit_system": {
"length": "km",
"mass": "kg",
"temperature": "°C",
"volume": "L"
},
"time_zone": "Europe/Amsterdam",
"components": ["light", "switch", "sensor", ...]
}
Render a template:
POST /api/template
Authorization: Bearer {token}
Content-Type: application/json
{
"template": "{{ states('sensor.temperature') }}"
}
Response (200 OK):
{
"template": "{{ states('sensor.temperature') }}",
"result": "22.5"
}
/api/websocketAuthentication:
// Message 1: Server sends (automatically)
{
"type": "auth_required",
"ha_version": "2025.1.0"
}
// Message 2: Client responds
{
"type": "auth",
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
// Message 3: Server confirms
{
"type": "auth_ok",
"ha_version": "2025.1.0"
}
Subscribe to events:
{
"id": 1,
"type": "subscribe_events",
"event_type": "state_changed"
}
// Responses come as:
{
"id": 1,
"type": "event",
"event": {
"type": "state_changed",
"data": {
"entity_id": "light.living_room",
"old_state": { "state": "off", ... },
"new_state": { "state": "on", ... }
}
}
}
Call a service:
{
"id": 2,
"type": "call_service",
"domain": "light",
"service": "turn_on",
"service_data": {
"entity_id": "light.living_room",
"brightness": 200
}
}
// Response:
{
"id": 2,
"type": "result",
"success": true,
"result": [
{
"entity_id": "light.living_room",
"state": "on",
"attributes": { ... }
}
]
}
Get current states:
{
"id": 3,
"type": "get_states"
}
// Response:
{
"id": 3,
"type": "result",
"success": true,
"result": [ ... ] // Array of all entity states
}
Subscribe to specific trigger:
{
"id": 4,
"type": "subscribe_trigger",
"trigger": {
"platform": "state",
"entity_id": "light.living_room"
}
}
// Trigger fires when light state changes:
{
"id": 4,
"type": "event",
"event": { ... }
}
import requests
import os
HA_URL = os.getenv("HA_URL", "http://localhost:8123")
HA_TOKEN = os.getenv("HA_TOKEN")
headers = {
"Authorization": f"Bearer {HA_TOKEN}",
"Content-Type": "application/json"
}
# Get all states
response = requests.get(f"{HA_URL}/api/states", headers=headers)
response.raise_for_status()
states = response.json()
# Get single entity
response = requests.get(f"{HA_URL}/api/states/light.living_room", headers=headers)
light_state = response.json()
print(f"Light state: {light_state['state']}")
print(f"Brightness: {light_state['attributes'].get('brightness', 'N/A')}")
# Call service
service_data = {
"entity_id": "light.living_room",
"brightness": 180,
"transition": 2
}
response = requests.post(
f"{HA_URL}/api/services/light/turn_on",
headers=headers,
json=service_data
)
response.raise_for_status()
print(f"Service call successful: {response.json()}")
# Handle errors
try:
response = requests.get(f"{HA_URL}/api/states/invalid.entity", headers=headers)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
if e.response.status_code == 401:
print("ERROR: Token invalid or expired")
elif e.response.status_code == 404:
print("ERROR: Entity not found")
else:
print(f"ERROR: {e}")
import asyncio
import aiohttp
import json
import os
HA_URL = os.getenv("HA_URL", "http://localhost:8123").replace("http", "ws")
HA_TOKEN = os.getenv("HA_TOKEN")
async def monitor_light_changes():
async with aiohttp.ClientSession() as session:
async with session.ws_connect(f"{HA_URL}/api/websocket") as ws:
# Wait for auth_required
msg = await ws.receive_json()
print(f"Server: {msg}")
# Send auth
await ws.send_json({
"type": "auth",
"access_token": HA_TOKEN
})
# Wait for auth_ok
msg = await ws.receive_json()
print(f"Server: {msg}")
# Subscribe to state changes
await ws.send_json({
"id": 1,
"type": "subscribe_events",
"event_type": "state_changed"
})
# Listen for events
async for msg in ws:
event = msg.json()
if event.get("type") == "event":
data = event["event"]["data"]
print(f"Entity: {data['entity_id']}")
print(f" Old state: {data['old_state']['state']}")
print(f" New state: {data['new_state']['state']}")
# Run the monitoring loop
asyncio.run(monitor_light_changes())
const HA_URL = process.env.HA_URL || "http://localhost:8123";
const HA_TOKEN = process.env.HA_TOKEN;
const headers = {
"Authorization": `Bearer ${HA_TOKEN}`,
"Content-Type": "application/json"
};
// Get all states
async function getAllStates() {
const response = await fetch(`${HA_URL}/api/states`, { headers });
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
// Get single entity
async function getEntityState(entityId) {
const response = await fetch(`${HA_URL}/api/states/${entityId}`, { headers });
if (response.status === 404) {
throw new Error(`Entity ${entityId} not found`);
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const state = await response.json();
console.log(`${entityId}: ${state.state}`);
console.log(`Attributes:`, state.attributes);
return state;
}
// Call service
async function callService(domain, service, data) {
const response = await fetch(
`${HA_URL}/api/services/${domain}/${service}`,
{
method: "POST",
headers,
body: JSON.stringify(data)
}
);
if (!response.ok) {
const error = await response.text();
throw new Error(`HTTP ${response.status}: ${error}`);
}
return response.json();
}
// Example usage
(async () => {
try {
const states = await getAllStates();
console.log(`Found ${states.length} entities`);
await getEntityState("light.living_room");
const result = await callService("light", "turn_on", {
entity_id: "light.living_room",
brightness: 180
});
console.log("Service call successful");
} catch (error) {
console.error(error);
}
})();
const HA_URL = (process.env.HA_URL || "http://localhost:8123").replace(/^http/, "ws");
const HA_TOKEN = process.env.HA_TOKEN;
async function subscribeToStateChanges() {
const ws = new WebSocket(`${HA_URL}/api/websocket`);
return new Promise((resolve, reject) => {
ws.onopen = () => console.log("WebSocket connected");
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log("Message:", msg);
if (msg.type === "auth_required") {
// Respond to auth challenge
ws.send(JSON.stringify({
type: "auth",
access_token: HA_TOKEN
}));
} else if (msg.type === "auth_ok") {
// Authentication successful, subscribe to events
ws.send(JSON.stringify({
id: 1,
type: "subscribe_events",
event_type: "state_changed"
}));
} else if (msg.type === "event" && msg.event?.type === "state_changed") {
// Handle state change
const data = msg.event.data;
console.log(`${data.entity_id} changed:`);
console.log(` ${data.old_state.state} → ${data.new_state.state}`);
}
};
ws.onerror = (error) => {
console.error("WebSocket error:", error);
reject(error);
};
ws.onclose = () => {
console.log("WebSocket closed");
resolve();
};
});
}
subscribeToStateChanges();
# Get all entities
curl -X GET "http://localhost:8123/api/states" \
-H "Authorization: Bearer ${HA_TOKEN}"
# Get specific entity
curl -X GET "http://localhost:8123/api/states/light.living_room" \
-H "Authorization: Bearer ${HA_TOKEN}"
# Turn on light with brightness
curl -X POST "http://localhost:8123/api/services/light/turn_on" \
-H "Authorization: Bearer ${HA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"entity_id": "light.living_room",
"brightness": 200,
"transition": 2
}'
# Turn off light
curl -X POST "http://localhost:8123/api/services/light/turn_off" \
-H "Authorization: Bearer ${HA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"entity_id": "light.living_room"}'
# Set climate temperature
curl -X POST "http://localhost:8123/api/services/climate/set_temperature" \
-H "Authorization: Bearer ${HA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"entity_id": "climate.living_room",
"temperature": 21
}'
# Get service schema
curl -X GET "http://localhost:8123/api/services/light" \
-H "Authorization: Bearer ${HA_TOKEN}"
# Call automation
curl -X POST "http://localhost:8123/api/services/automation/trigger" \
-H "Authorization: Bearer ${HA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"entity_id": "automation.my_automation"}'
# Render template
curl -X POST "http://localhost:8123/api/template" \
-H "Authorization: Bearer ${HA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"template": "{{ states(\"sensor.temperature\") }}"}'
| Issue | Root Cause | Solution |
|---|---|---|
| 401 Unauthorized | Token invalid, expired, or malformed | Create new token in Settings → Create Long-Lived Access Token; verify Bearer prefix |
| 404 Not Found | Entity doesn't exist or wrong domain | Check entity_id with GET /api/states; verify domain.service format |
| WebSocket auth timeout | Didn't respond to auth_required within 10s | Send auth message immediately upon receiving auth_required |
| Attribute confusion | Mixing state string with attributes object | State is always a string; attributes contain metadata (brightness, color, etc.) |
| Service call fails silently | Wrong domain/service or missing required fields | Use GET /api/services to discover available services and required parameters |
import requests
try:
response = requests.get(f"{HA_URL}/api/states/{entity_id}", headers=headers)
if response.status_code == 401:
print("Token invalid/expired - create new token in HA UI")
elif response.status_code == 403:
print("Forbidden - token lacks necessary permissions")
elif response.status_code == 404:
print("Entity not found - check entity_id")
elif response.status_code == 502:
print("Home Assistant unavailable - check server status")
elif response.status_code >= 500:
print("Server error - try again later")
else:
response.raise_for_status()
return response.json()
except requests.exceptions.ConnectionError:
print("Cannot connect to Home Assistant - verify HA_URL and network")
except requests.exceptions.Timeout:
print("Request timeout - Home Assistant is slow to respond")
const ws = new WebSocket(wsUrl);
ws.onerror = (error) => {
console.error("WebSocket error:", error);
// Reconnect after delay
setTimeout(connect, 5000);
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "result" && !msg.success) {
console.error("Command failed:", msg.error);
}
};
ws.onclose = () => {
console.log("Connection closed");
// Implement reconnection logic
setTimeout(connect, 3000);
};
Located in references/:
REST_API_REFERENCE.md - Complete REST endpoint documentationWEBSOCKET_API_REFERENCE.md - WebSocket command referenceAUTHENTICATION.md - Auth token creation and security best practicesSERVICE_CATALOG.md - Common services by domainNote: For deep dives on specific topics, see the reference files above.
| Package | Language | Purpose |
|---|---|---|
| requests | Python | HTTP client for REST API calls |
| aiohttp | Python | Async HTTP/WebSocket client |
| fetch | JavaScript | Native HTTP client (browser/Node.js) |
| WebSocket | JavaScript | Native WebSocket API (browser/Node.js) |
| Package | Language | Purpose |
|---|---|---|
| httpx | Python | Advanced HTTP client with streaming |
| websockets | Python | Pure Python WebSocket library |
| axios | JavaScript | Promise-based HTTP client |
Symptoms: All API requests return 401, even with correct token.
Solution:
echo $HA_TOKEN # Verify token is set
echo $HA_URL # Verify URL is correct (http not https)
Symptoms: WebSocket connects then closes without response.
Solution:
/api/websocket path, not /api/websocket/// Incorrect path
ws = new WebSocket("ws://ha:8123/api/websocket/");
// Correct path
ws = new WebSocket("ws://ha:8123/api/websocket");
Symptoms: State should be "on"/"off" but appears as number or boolean.
Solution:
state = entity["state"] # String: "on", "off", "123"
is_on = state == "on" # Convert to boolean
# For numeric states
temperature = float(entity["state"]) # Convert to float
Symptoms: Service call returns HTTP 200 but nothing happens.
Solution:
GET /api/services/{domain}# Correct - entity_id in service_data
curl -X POST "http://ha:8123/api/services/light/turn_on" \
-d '{"entity_id": "light.living_room"}'
# Incorrect - entity_id as path parameter
curl -X POST "http://ha:8123/api/services/light/turn_on/light.living_room"
Before using this skill, verify: