Home Assistant
Connects one or more Home Assistant instances to Clapilot — shared dashboard tiles, entity browsing, camera snapshots, device control, and spoken announcements on Home Assistant voice satellites and speakers via the REST API.
What it does
home-assistant is a bundled iframe module that connects Clapilot to one or more Home Assistant
instances over the Home Assistant REST API. Per connected instance it provides:
- a shared Dashboard of tiles bound to Home Assistant entities: camera snapshot tiles, toggle and
control tiles (light, switch, fan, cover, lock, climate, media player), sensor value tiles, and generic
state tiles, with rename, resize (
small/wide), reorder (move up/down), and remove, plus a per-tile detail dialog with drag sliders for position, brightness, speed, temperature, and volume; - a searchable, domain-filterable Geräte / Entities list of every entity the instance exposes
(
GET /api/states) with state and friendly name, plus an "add to dashboard" action per entity; - Sprachgeräte / Voice targets: Home Assistant assist satellites and media players that agents can use for spoken announcements, with discovery, enable/disable, TTS engine selection for media players, and a test announcement.
The module UI is tabbed — Dashboard, Geräte, Sprachgeräte — and localized in German, English, and
Italian. Connection management is not part of the module UI; it lives under
Settings → Module → Home Assistant. When no connection exists the module shows an empty state with a
link to /settings/home-assistant.
Connections, dashboard tiles, and voice targets are workspace-global (org-level): every
authenticated user of the instance sees the same connected instances and works on the same shared
dashboard. created_by on all three tables is attribution/audit only and is never used as a read/write
filter.
The module is not installed by default. It appears in the Module Store as a bundled module and must
be enabled by an admin; a global migration (db/migrations/322_home_assistant_module_default_off.sql)
seeds a module_installs override so it also stays off on instances using the legacy_all install
policy. Installing the module runs its schema migration
(bundled-modules/home-assistant/migrations/001_create_home_assistant_schema.sql), which creates the
home_assistant_connections, home_assistant_dashboard_tiles, and home_assistant_voice_targets
tables. The module handler also bootstraps the schema on first use (with module_schema_migrations
bookkeeping and an advisory lock), so it works even if install-time migrations were skipped.
Connecting an instance
Admins connect instances under Settings → Module → Home Assistant (/settings/home-assistant, admin
only, module-gated). Each connection has a name, a base URL, and a long-lived access token:
- In Home Assistant, open your user profile (
Profil → Sicherheit/Profile → Security), scroll to Long-lived access tokens, and create a token for Clapilot. Copy it once — Home Assistant does not show it again. - In Clapilot, enter the instance name, the base URL (for example
http://homeassistant.local:8123orhttps://ha.example.com), and the token. LAN and private hosts are allowed — Home Assistant usually runs on the local network. The base URL must behttp:orhttps:with a host, no credentials, and no path or query; trailing slashes are stripped. - Save. The connection is verified live through
GET /api/configbefore it is stored; the response'slocation_nameandversionare recorded on the connection together withlast_verified_at. The first connection becomes the default instance for all list endpoints and tools that do not pass aconnection_id.
Per connection the settings panel offers verify, edit (name, URL, new token), set default, and delete (two-step inline confirm). Deleting a connection cascades its tiles and voice targets; if it was the default, the oldest remaining connection is promoted. A "test connection" action verifies a URL/token pair without saving it.
All Home Assistant requests are made server-side by the module handler with
Authorization: Bearer <token> and a 15-second timeout. Authentication failures (401/403 from Home
Assistant) surface as 422 home_assistant_auth_failed, network errors and timeouts as
502 home_assistant_unreachable, and other non-2xx answers as 502 home_assistant_error; the last error
is also stored on the connection.
Credential storage
Long-lived access tokens are stored AES-256-GCM-encrypted in the credentials JSONB column of
home_assistant_connections, keyed from HOME_ASSISTANT_TOKEN_ENCRYPTION_SECRET (falling back to
AUTH_SECRET) via HKDF. Tokens are never returned by any API or agent tool: connection objects only
expose has_token: true. Rotating a token in Home Assistant requires editing the connection with the new
token.
Dashboard
The dashboard is one shared tile grid per connection. Tiles reference an entity (entity_id, unique per
connection) and carry a tile_kind, an optional title override (otherwise the entity's friendly name), a
size (small | wide), a position, and a free-form config object. The tile kind defaults from the
entity domain when a tile is added:
| Entity domain | Tile kind | Tile actions |
|---|---|---|
camera.* | camera | Live snapshot via the module's camera proxy (cache-control: no-store) |
switch, input_boolean, siren, automation | toggle | Turn on / off / toggle |
light.* | toggle | Turn on / off / toggle, brightness (set_brightness, data.brightness_pct 0–100), colour temperature (set_color_temp, data.color_temp_kelvin 1000–10000) — both sent as light.turn_on |
fan.* | toggle | Turn on / off / toggle, speed (set_percentage, data.percentage 0–100) |
humidifier.* | toggle | Turn on / off / toggle, target humidity (set_humidity, data.humidity 0–100) |
script, scene | toggle | Activate only (no state) |
cover.* | cover | Open / close / stop (toggle maps to cover.toggle), position (set_position, data.position 0–100), tilt (set_tilt_position, data.tilt_position 0–100, plus open_tilt / close_tilt) |
lock.* | lock | Lock / unlock |
climate.* | climate | Target temperature (set_temperature), HVAC mode (set_hvac_mode), fan mode (set_fan_mode) |
media_player.* | media_player | Play / pause / play-pause, track skip, volume (volume_set, data.volume_level 0–1), mute |
sensor, binary_sensor, weather, person, device_tracker, sun | sensor | Value display only |
| everything else | state | Raw state display |
Tile states are fetched from Home Assistant in a single GET /api/states call and merged into the tile
list. When the instance is unreachable, tiles are still returned with state: null and a
warning: "home_assistant_unreachable" so the layout stays editable.
Clicking a tile's name opens a detail dialog for that entity. The dialog offers large drag sliders for the continuous values the entity supports — cover position and tilt, light brightness, fan speed, climate target temperature, and media player volume — together with quick presets (0 / 25 / 50 / 75 / 100 %), open / stop / close for covers, and the entity's current attributes. The tile itself keeps its compact inline controls, so the common on/off and step actions stay one click away without opening the dialog.
Tile actions (POST dashboard/tiles/:id/action) are a convenience layer over
POST /api/services/<domain>/<service>; the state is re-read after each call. Numeric payloads are range
checked server-side and unsupported actions for a domain answer 400 validation_error. Any other service
can be invoked directly through POST services/call.
Voice targets
A voice target is a Home Assistant entity that can play a spoken message. Two kinds are supported:
| Kind | Entity | How the announcement is sent |
|---|---|---|
assist_satellite | assist_satellite.* (Home Assistant Voice PE, ESPHome satellites, …) | POST /api/services/assist_satellite/announce with { entity_id, message } (and preannounce: false when requested) |
media_player | media_player.* (speakers, Sonos, Chromecast, …) | POST /api/services/tts/speak with { entity_id: <tts_entity_id>, media_player_entity_id, message } — a tts.* engine entity is required |
Targets are discovered from the instance's states (GET voice-targets/discover lists satellites, media
players, and available tts.* engines), added with a display name, and can be enabled/disabled without
being removed. A disabled target is skipped by broadcast announcements and rejects direct announcements
with 409 target_disabled. Every announcement updates last_announced_at or last_error on the target,
and the settings panel offers a localized test announcement per target.
Home Assistant renders the speech. Announcements use Home Assistant's own TTS pipeline
(assist_satellite.announce / tts.speak); Clapilot's chat TTS is not involved. Playing Clapilot-generated
audio on a Home Assistant media player (media_player.play_media with a Clapilot audio URL) is future
work — Clapilot's /api/chat/audio/[id] is session-scoped and cannot be fetched by Home Assistant today.
Voice targets are output-only announcement endpoints and are distinct from paired Clapilot voice devices: a voice device is a Clapilot-owned speaker bound to one user that records speech and talks to the personal chat/live-voice endpoints, while a Home Assistant voice target only receives text that Home Assistant speaks. The two systems do not share tables, tokens, or settings screens.
API
The module API lives under /api/modules/home-assistant/api and is authenticated like all module handler
APIs (session cookie or agent service auth); the iframe UI, the settings panels, and the agent tools all
use it. All endpoints return {"error":"module_not_installed"} while the module is disabled. Connection
create/update/delete and the connection test are admin-only (403 forbidden otherwise; re-verifying a stored
connection is open to every user);
reading entities, controlling devices, managing tiles and voice targets, and announcing are available to
every authenticated user. List endpoints accept an optional connection_id and fall back to the default
connection; with no connection at all they answer 409 no_connection.
See API Reference for the endpoint list: connections (list/create/update/verify/
delete/test), entities (list with search/domain/limit, domain counts, entity detail), services/call,
camera/:entity_id/snapshot (binary image), dashboard (tile list with merged states, tile
create/update/move/delete/action), voice targets (list/discover/create/update/delete/announce), and the
multi-target announce endpoint. Error codes: 409 no_connection, 404 connection_not_found,
400 invalid_base_url, 400 token_required, 422 home_assistant_auth_failed,
502 home_assistant_unreachable, 502 home_assistant_error, 400 invalid_entity_id,
400 invalid_service, 404 tile_not_found, 409 tile_exists, 404 target_not_found,
409 target_disabled, 400 tts_entity_required, 400 unsupported_voice_entity, 403 forbidden, and
400 validation_error. Error bodies are { error: <code>, message?: <human string> }.
Agent tools
home_assistant_list_connections, home_assistant_list_entities, home_assistant_get_state,
home_assistant_call_service, home_assistant_list_dashboard_tiles,
home_assistant_add_dashboard_tile, home_assistant_remove_dashboard_tile,
home_assistant_list_voice_targets, and home_assistant_announce are install-gated on this module
(native bundle home_assistant) and run against the module API above, so chat agents and live-voice
agents can read states, control devices, manage the shared dashboard, and send spoken announcements to
Home Assistant voice satellites and speakers.
home_assistant_call_serviceis mutating — it changes real devices in the home immediately, so the agent should confirm the target entity and action with the user before calling it.home_assistant_add_dashboard_tileandhome_assistant_remove_dashboard_tilemutate the shared dashboard that every workspace user sees.home_assistant_announcespeaks a message on the named targets (target_idsortarget_names, matched case-insensitively against target name and entity id) or on all enabled targets when no target is given; the result lists per-target success or error.
Connecting an instance is intentionally not an agent tool — access tokens must not pass through chat; admins connect instances in Settings. See Agent Tool Contracts for the full contracts.
Inbound automations
Home Assistant can also trigger Clapilot. A Home Assistant automation with a rest_command or the
built-in webhook action can call an existing Clapilot webhook automation at
/api/automation-webhooks/<token> (POST with a JSON body); Clapilot runs the linked scheduled task with
the payload as event context. See Tasks and the automation-webhook entry in the
API Reference for creating a webhook automation and obtaining its token. No
Home-Assistant-specific inbound endpoint is needed.
Apple clients
Both surfaces are native SwiftUI; the Apple clients never load the module's web page.
The settings surface lists Home Assistant in Settings → Module (gated like the other module settings
entries on install state and admin role) and provides the connection list with add/edit/verify/delete plus
voice-target management (list, enable toggle, discover/add, test announcement).
The module itself is a native screen in the app's side menu (MainAppSection.homeAssistant, shown only
while the module is installed, Views/HomeAssistantView.swift). It mirrors the web module: a Dashboard
mode with the shared tiles and their inline controls, a Geräte mode with the searchable,
domain-filterable entity list and add-to-dashboard, and a Sprachgeräte mode for the voice targets.
Tapping a tile or an entity opens the native entity detail sheet
(Views/HomeAssistantEntityDetailView.swift) with the same large drag slider as the web dialog — filled
from the top for cover position, from the bottom for brightness, fan speed, and target humidity — plus the
quick presets, per-domain controls, and the attribute list. Tile mutations go through the tile-action
endpoint; controls opened from an entity row call POST services/call with the equivalent service. The
dashboard polls every 15 s, camera tiles every 10 s, and the open detail sheet every 6 s, each only while
the scene is active.
