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:

  1. 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.
  2. In Clapilot, enter the instance name, the base URL (for example http://homeassistant.local:8123 or https://ha.example.com), and the token. LAN and private hosts are allowed — Home Assistant usually runs on the local network. The base URL must be http: or https: with a host, no credentials, and no path or query; trailing slashes are stripped.
  3. Save. The connection is verified live through GET /api/config before it is stored; the response's location_name and version are recorded on the connection together with last_verified_at. The first connection becomes the default instance for all list endpoints and tools that do not pass a connection_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 domainTile kindTile actions
camera.*cameraLive snapshot via the module's camera proxy (cache-control: no-store)
switch, input_boolean, siren, automationtoggleTurn on / off / toggle
light.*toggleTurn 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.*toggleTurn on / off / toggle, speed (set_percentage, data.percentage 0–100)
humidifier.*toggleTurn on / off / toggle, target humidity (set_humidity, data.humidity 0–100)
script, scenetoggleActivate only (no state)
cover.*coverOpen / 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.*lockLock / unlock
climate.*climateTarget temperature (set_temperature), HVAC mode (set_hvac_mode), fan mode (set_fan_mode)
media_player.*media_playerPlay / pause / play-pause, track skip, volume (volume_set, data.volume_level 0–1), mute
sensor, binary_sensor, weather, person, device_tracker, sunsensorValue display only
everything elsestateRaw 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:

KindEntityHow the announcement is sent
assist_satelliteassist_satellite.* (Home Assistant Voice PE, ESPHome satellites, …)POST /api/services/assist_satellite/announce with { entity_id, message } (and preannounce: false when requested)
media_playermedia_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_service is 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_tile and home_assistant_remove_dashboard_tile mutate the shared dashboard that every workspace user sees.
  • home_assistant_announce speaks a message on the named targets (target_ids or target_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.