plugins/home-assistant-dev/skills/ha-websocket-api/SKILL.md
Implement WebSocket API commands for Home Assistant integrations. Use when asked about WebSocket API, custom API endpoints, frontend integration, custom panels, or real-time data to frontend.
npx skillsauth add l3digital-net/claude-code-plugins ha-websocket-apiInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Create custom WebSocket API commands for frontend integration, custom panels, or third-party tools.
Use WebSocket API for:
# api.py
"""WebSocket API for {Name}."""
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from .const import DOMAIN
async def async_setup_api(hass: HomeAssistant) -> None:
"""Set up WebSocket API."""
websocket_api.async_register_command(hass, websocket_get_devices)
websocket_api.async_register_command(hass, websocket_get_device_data)
websocket_api.async_register_command(hass, websocket_subscribe_updates)
@websocket_api.websocket_command(
{
vol.Required("type"): f"{DOMAIN}/devices",
}
)
@callback
def websocket_get_devices(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Return list of devices."""
# Get data from your integration
devices = []
for entry_id, coordinator in hass.data.get(DOMAIN, {}).items():
for device_id, device in coordinator.devices.items():
devices.append({
"id": device_id,
"name": device.get("name"),
"online": device.get("online", False),
})
connection.send_result(msg["id"], {"devices": devices})
@websocket_api.websocket_command(
{
vol.Required("type"): f"{DOMAIN}/device/data",
vol.Required("device_id"): str,
}
)
@websocket_api.async_response
async def websocket_get_device_data(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Return data for a specific device."""
device_id = msg["device_id"]
# Find device data
device_data = None
for coordinator in hass.data.get(DOMAIN, {}).values():
if device_id in coordinator.devices:
device_data = coordinator.devices[device_id]
break
if device_data is None:
connection.send_error(msg["id"], "not_found", f"Device {device_id} not found")
return
connection.send_result(msg["id"], device_data)
# __init__.py
from .api import async_setup_api
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up integration."""
# ... your setup code ...
# Register WebSocket API (only once)
if DOMAIN not in hass.data:
await async_setup_api(hass)
# ... rest of setup ...
For real-time updates to the frontend:
@websocket_api.websocket_command(
{
vol.Required("type"): f"{DOMAIN}/subscribe",
vol.Optional("device_id"): str,
}
)
@websocket_api.async_response
async def websocket_subscribe_updates(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Subscribe to updates."""
device_id = msg.get("device_id")
@callback
def async_handle_update() -> None:
"""Handle coordinator update."""
# Send update to client
connection.send_message(
websocket_api.event_message(
msg["id"],
{"event": "update", "device_id": device_id},
)
)
# Subscribe to coordinator updates
for coordinator in hass.data.get(DOMAIN, {}).values():
if device_id is None or device_id in coordinator.devices:
unsub = coordinator.async_add_listener(async_handle_update)
# Unsubscribe when connection closes
connection.subscriptions[msg["id"]] = unsub
# Send initial confirmation
connection.send_result(msg["id"])
from homeassistant.components.websocket_api import (
ERR_INVALID_FORMAT,
ERR_NOT_FOUND,
ERR_UNKNOWN_ERROR,
)
@websocket_api.websocket_command({...})
@websocket_api.async_response
async def websocket_command(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Handle command."""
try:
result = await do_something()
connection.send_result(msg["id"], result)
except ValueError as err:
connection.send_error(msg["id"], ERR_INVALID_FORMAT, str(err))
except KeyError:
connection.send_error(msg["id"], ERR_NOT_FOUND, "Resource not found")
except Exception as err:
connection.send_error(msg["id"], ERR_UNKNOWN_ERROR, str(err))
By default, WebSocket commands require authentication. For admin-only commands:
@websocket_api.websocket_command(
{
vol.Required("type"): f"{DOMAIN}/admin/config",
}
)
@websocket_api.require_admin
@websocket_api.async_response
async def websocket_admin_config(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Admin-only command."""
# Only admins can call this
pass
From a custom panel or card:
// JavaScript example
const conn = await hass.connection;
// Simple command
const result = await conn.sendMessagePromise({
type: "my_integration/devices",
});
console.log(result.devices);
// Command with parameters
const deviceData = await conn.sendMessagePromise({
type: "my_integration/device/data",
device_id: "device_123",
});
// Subscription
const unsub = conn.subscribeMessage(
(message) => {
console.log("Update received:", message);
},
{ type: "my_integration/subscribe" }
);
// Later: unsub() to stop subscription
from homeassistant.components.websocket_api import TYPE_RESULT
async def test_websocket_get_devices(
hass: HomeAssistant,
hass_ws_client,
) -> None:
"""Test get devices command."""
client = await hass_ws_client(hass)
await client.send_json({"id": 1, "type": f"{DOMAIN}/devices"})
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"] is True
assert "devices" in msg["result"]
my_integration/actionha-service-actionsha-coordinatordevelopment
Use when you're stuck or missing current information mid-task - the same command/API/approach failed twice, an error looks like a changed or deprecated API, or you need the current version of something, a fact from after your training cutoff, or to verify something you cannot confirm from the code in context. Starts with a cheap inline lookup and only escalates to a full research sweep if that fails. Do not use for routine pre-emptive checks before ordinary library work - for deliberate research, use /qdev:research.
documentation
Update Outline wiki documentation with implementation-level details from the current session by dispatching the up-docs-propagate-wiki sub-agent. This skill should be used when the user runs /up-docs:wiki.
documentation
Update repository documentation (README.md, docs/, CLAUDE.md) based on session changes by dispatching the up-docs-propagate-repo sub-agent. This skill should be used when the user runs /up-docs:repo.
documentation
Update Notion pages with strategic and organizational context from the current session by dispatching the up-docs-propagate-notion sub-agent. This skill should be used when the user runs /up-docs:notion.