Home Assistant

Connects one or more Home Assistant instances to Clapilot — shared dashboard tiles, entity browsing, camera snapshots, device control, automation management (including automations that start Clapilot), 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, remove, and a per-tile detail dialog with drag sliders for position, brightness, speed, temperature, and volume. The layout is customizable like the main Dashboard: drag a tile by its header grip to reorder it, drag the bottom-right corner handle to resize it in whole columns/rows, or use the tile menu (size presets Small 1×1, Wide 2×1, Tall 1×2, Large 2×2, Full width 4×1, plus Move up/down; the grip also accepts arrow keys). Tiles flow in order on a 1–4 column grid that follows the available width; spans wider than the current column count are clamped for display only. The iOS/macOS client offers the same size presets plus drag-and-drop reordering;
  • 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;
  • Automationen / Automations: every Home Assistant automation with on/off switch and "run now", a YAML editor to create, edit, and delete automations, and the setup for automations that start Clapilot automations (see Automations).

The module UI is tabbed — Dashboard, Geräte, Sprachgeräte, Automationen — 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; 002_managed_automations.sql adds home_assistant_managed_automations, which remembers the automations created through Clapilot (connection, Home Assistant config id, created_by/updated_by for attribution). 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.

Reading states, controlling devices, and announcing work with any Home Assistant user's token. Managing automations uses Home Assistant's admin-only config API, so create the token as a Home Assistant administrator if Clapilot should create or edit automations; with a non-admin token the automation editor answers home_assistant_admin_required while everything else keeps working.

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, and automations (list with optional bridge status, bridge status, detail with config/config_yaml, create, update, delete, and enable/disable/trigger actions). 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, 404 automation_not_found, 409 automation_not_editable, 409 automation_not_loaded, 400 automation_invalid, 422 home_assistant_admin_required, 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_update_dashboard_tile, home_assistant_remove_dashboard_tile, home_assistant_list_voice_targets, home_assistant_announce, home_assistant_list_automations, home_assistant_get_automation, home_assistant_save_automation, home_assistant_delete_automation, home_assistant_set_automation_enabled, and home_assistant_trigger_automation 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, and create and manage Home Assistant automations.

  • 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, home_assistant_update_dashboard_tile (rename, resize via size, reorder via 0-based position), 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.
  • home_assistant_save_automation (create without automation_id, update with it — top-level keys replace, null removes) and home_assistant_delete_automation need the user's approval: they are classified outbound and delete for the agent action approval gate, because a saved automation keeps acting on real devices without anyone watching. Unattended runs can only use them when the scheduled task pre-approved them. home_assistant_list_automations also reports the bridge status and snippet so the agent can explain the one-time setup.
  • home_assistant_set_automation_enabled and home_assistant_trigger_automation switch or run an automation immediately (ordinary writes, like home_assistant_call_service).

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.

Automations

The Automationen tab lists every automation.* entity of the instance, sorted by name, with its last trigger time, a "Clapilot" badge for automations created through Clapilot, and a "YAML" badge for automations without a config id. Each row has an on/off switch (automation.turn_on / automation.turn_off) and a run button (automation.trigger, which skips the conditions like Home Assistant's "Run actions").

Selecting an automation opens its detail with the full configuration in a YAML editor — the same format as Home Assistant's "Edit in YAML". New automation opens the editor with a small template (a daily time trigger that creates a persistent notification). Saving sends the configuration to Home Assistant, which validates it, writes automations.yaml, and reloads the automation; Home Assistant's validation message is shown under the editor when it rejects a configuration. Delete is a two-step inline confirm.

Clapilot manages automations through the config API that Home Assistant's own automation editor uses (/api/config/automation/config/<id>). It is not part of Home Assistant's documented REST API but has been stable for years because the editor depends on it. Consequences:

  • only automations stored in automations.yaml (the ones created in the Home Assistant editor or in Clapilot) are editable. Automations defined in other YAML files or packages are shown read-only; they can still be switched on/off and run;
  • the connection's token must belong to a Home Assistant administrator (see Connecting an instance);
  • saving replaces the whole automation in Home Assistant; the agent tool merges top-level keys on top of the stored configuration first. Legacy singular keys (trigger, condition, action) are saved in the plural form of Home Assistant 2024.10+, the minimum supported version for editing.

Starting Clapilot from Home Assistant

A Home Assistant automation can start a Clapilot automation through Clapilot's existing webhook trigger (/api/automation-webhooks/<token>, see Tasks). Home Assistant has no built-in action that sends an HTTP request without YAML configuration, so an administrator adds this one-time rest_command block to Home Assistant's configuration.yaml and restarts Home Assistant:

rest_command:
  clapilot:
    url: "https://your-clapilot-host/api/automation-webhooks/{{ token }}"
    method: post
    content_type: "application/json"
    payload: "{{ (payload | default({})) | tojson }}"
    timeout: 20

The tab shows the status of this bridge (Home Assistant's service list contains rest_command.clapilot or not) and the block with the instance's real address filled in (the public base URL from the settings) behind Show setup, with a copy button. After that, any automation can call:

actions:
  - action: rest_command.clapilot
    data:
      token: "<webhook token of the Clapilot automation>"
      payload:
        automation: "{{ this.attributes.friendly_name }}"
        entity_id: "{{ trigger.entity_id | default('') }}"
        description: "{{ trigger.description | default('') }}"

The editor's Start a Clapilot automation helper lists the Clapilot automations with a webhook trigger and renders this action with the right token to copy. Clapilot receives payload as the webhook body and runs the linked automation with it as event context. Saving an automation that calls rest_command.clapilot while the bridge is missing succeeds (Home Assistant only checks services when the action runs) but returns the warning clapilot_bridge_missing, which the editor shows. The webhook token is a secret: whoever knows it can start that Clapilot automation.

The agent does the same in one conversation: it creates the Clapilot automation with a webhook trigger (scheduled_tasks_create), then the Home Assistant automation with the rest_command.clapilot action (home_assistant_save_automation), after the user approved it.

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, a Sprachgeräte mode for the voice targets, and an Automationen mode (Views/HomeAssistantAutomationViews.swift) with the automation list (on/off switch, run button, Clapilot/YAML badges, bridge status with copyable setup block) and a detail screen — full-screen on iPhone, a sheet on iPad and Mac — with the YAML editor (smart quotes and autocorrection off), save, run, on/off, and delete with confirmation; "+" opens the editor with the template. 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.