Tool Explorer
Browse all 87 MCP tools across 29 categories.
Add-ons
Description
Get Home Assistant add-ons - list installed, available, or get details for one. This tool retrieves add-on information based on the parameters: - slug provided: Returns detailed info for a single add-on (ingress, ports, options, state) - source='installed' (default): Lists currently installed add-ons - source='available': Lists add-ons available in the add-on store **Note:** This tool only works with Home Assistant OS or Supervised installations. **SINGLE ADD-ON (slug provided):** Returns comprehensive details including ingress entry, ports, options, state, and (when the add-on exposes one) a top-level ``log_level`` reflecting the current Supervisor option — useful for confirming ha_manage_addon log_level changes. Useful for discovering what APIs an add-on exposes before calling ha_manage_addon. **INSTALLED ADD-ONS (source='installed'):** Returns add-ons with version, state (started/stopped), and update availability. - include_stats: Optionally include CPU/memory usage statistics **AVAILABLE ADD-ONS (source='available'):** Returns add-ons from official and custom repositories that can be installed. - repository: Filter by repository slug (e.g., 'core', 'community') - query: Search by name or description (case-insensitive) **Example Usage:** - List installed add-ons: ha_get_addon() - Get Node-RED details: ha_get_addon(slug="<prefix>_nodered") - List with resource usage: ha_get_addon(include_stats=True) - List available add-ons: ha_get_addon(source="available") - Search for MQTT: ha_get_addon(source="available", query="mqtt")
Parameters
source-(Annotated[Literal['installed', 'available'] | None, Field(description="Add-on source: 'installed' (default) for currently installed add-ons, 'available' for add-ons in the store that can be installed.", default=None)])= nullslug-(Annotated[str | None, Field(description="Add-on slug for detailed info (e.g., '<prefix>_nodered'). Slug prefixes vary by add-on repository — omit to list all add-ons and discover the actual installed slug.", default=None)])= nullinclude_stats-(Annotated[bool, Field(description="Include CPU/memory usage statistics (only for source='installed')", default=False)])= falserepository-(Annotated[str | None, Field(description="Filter by repository slug, e.g., 'core', 'community' (only for source='available')", default=None)])= nullquery-(Annotated[str | None, Field(description="Search filter for add-on names/descriptions (only for source='available')", default=None)])= nullDescription
Manage a Home Assistant add-on — update its configuration or call its internal API. Five mutually exclusive operating modes: **Lifecycle mode** (when ``action`` is one of install/uninstall/start/ stop/restart/rebuild/update): Runs a Supervisor add-on action on ``slug``. ``install`` / ``update`` go through the store (the add-on's repository must be registered — it shows up in ``ha_get_addon(source="available")``); the rest act on an installed add-on. This is how an assistant brings an add-on online for the user (e.g. installing + starting the dashboard screenshot engine). **Store-repository mode** (when ``action`` is ``add_repository`` or ``remove_repository``): Registers or unregisters a custom add-on store repository. These actions operate on the store rather than an installed add-on, so they take the ``repository`` param and no ``slug``: ``add_repository`` POSTs the repository URL to ``/store/repositories``; ``remove_repository`` DELETEs ``/store/repositories/{slug}`` by the repository's slug. Adding a repository (e.g. balloob's add-ons) is the missing step that lets an assistant then install an add-on from it via ``action="install"``. **Config mode** (when any of options/network/boot/auto_update/watchdog is provided): Updates the add-on's Supervisor configuration via POST /addons/{slug}/options. All config parameters are optional; only provided fields are updated — current values are fetched and merged automatically (including one level of nested dicts). **Proxy mode** (when path is provided without array_patch): Routes HTTP or WebSocket requests through Home Assistant's Ingress proxy by default (works on HAOS, Supervised, and off-host PyPI/uvx installs). Pass `port=...` to bypass Ingress and connect directly to an add-on's container port — that mode requires the MCP host to share Home Assistant's container network (i.e. only the HAOS addon). Use ha_get_addon(slug="...") to discover available ports and endpoints. **ESPHome Device Builder dashboard (current rewrite):** config and log access is a WebSocket JSON-command API, NOT REST. The legacy endpoints are gone — `GET /edit?configuration=` now returns the dashboard SPA, and the old `/compile` `/validate` `/logs` WebSocket paths (which took `{"type": "spawn", ...}` bodies) reject the upgrade (HTTP 200). Use instead: - HTTP `GET /devices` → JSON list of configured devices; each entry's `configuration` field is the YAML filename to pass below. - WebSocket `path="/ws"` with body `{"command": "<cmd>", "message_id": "1", "args": {...}}`. The server sends a `server_info` message first, then one reply per `message_id`. Wire-confirmed commands: `devices/get_config` `{configuration}` → raw YAML (in the reply's `result`); `devices/logs` (stream) `{configuration, port: "OTA"}` → live device logs. Also exposed by the dashboard frontend (command/arg names not wire-tested here): `devices/update_config` `{configuration, content}` → save, `devices/validate`, `firmware/compile`. - The `/ws` channel stays open, so for a one-shot read or a bounded log capture pass `wait_for_close=False` with `message_limit` (and `message_offset` to skip the server_info / config-banner preamble). Reach the dashboard through Ingress — omit `port`; direct `port=` does not route to it. **Array-patch mode** (when path AND array_patch are provided): Atomic "GET array, mutate, POST array" workflow for addon APIs whose write contract is "send the whole resource collection back". Operations are applied in order to a working copy; if any op fails validation (unknown id, collision, malformed shape) nothing is posted. Returns a compact summary instead of the full array. Designed for Node-RED /flows and similar endpoints. **Response shaping (proxy mode):** - WebSocket streams can be noisy (e.g. the ESPHome dashboard's devices/logs dumps the device's full config banner on connect). By default, `summarize=True` collapses long runs of non-signal messages into short elision markers; INFO/WARNING/ERROR/exit lines always pass through. Pagination via `message_offset` / `message_limit` works on the raw collected list before summarize runs. - `python_transform` applies a sandboxed Python expression as a final post-processing step in both HTTP and WebSocket modes. The variable `response` is bound to: * WebSocket: `list[dict | str]` — parsed JSON messages are dicts, undecodable frames stay as ANSI-stripped strings. Elision markers appear as `{"elided": N, "note": "..."}` dicts when summarize ran. * HTTP: `dict | list | str` — whichever the content-type produced. Transforms may mutate in place (response.append(...), del response[k]) or reassign (response = [...]). This is post-processing only — it does NOT provide optimistic-locking or write-back semantics. **WARNING:** Setting boot="auto"/"manual" will fail for add-ons whose Supervisor metadata locks the boot mode. The Supervisor returns an error in this case. **NOTE:** This tool only works with Home Assistant OS or Supervised installations. **Examples:** - Install an add-on: ha_manage_addon(slug="...", action="install") - Start an add-on: ha_manage_addon(slug="...", action="start") - Add a store repository: ha_manage_addon(action="add_repository", repository="https://github.com/balloob/home-assistant-addons") - Remove a store repository: ha_manage_addon(action="remove_repository", repository="0f1cc410") - Set add-on option: ha_manage_addon(slug="...", options={"log_level": "debug"}) Note: only the fields you provide are updated — current values are fetched first and merged automatically. Fields not in the add-on's schema are ignored with a warning. - Disable auto-update: ha_manage_addon(slug="...", auto_update=False) - Change host port: ha_manage_addon(slug="...", network={"5800/tcp": 8082}) - Set boot mode: ha_manage_addon(slug="...", boot="manual") - Call HTTP API: ha_manage_addon(slug="...", path="/api/events") - Direct port: ha_manage_addon(slug="...", path="/flows", port=1880) - ESPHome list devices (HTTP): ha_manage_addon(slug="<prefix>_esphome", path="/devices") - ESPHome read a device's YAML (WS one-shot): ha_manage_addon(slug="<prefix>_esphome", path="/ws", websocket=True, wait_for_close=False, message_limit=2, body={"command": "devices/get_config", "message_id": "1", "args": {"configuration": "device.yaml"}}) - ESPHome live logs (WS, bounded): ha_manage_addon(slug="<prefix>_esphome", path="/ws", websocket=True, wait_for_close=False, message_limit=60, body={"command": "devices/logs", "message_id": "1", "args": {"configuration": "device.yaml", "port": "OTA"}}) - Filter WS errors only: ha_manage_addon(slug="...", path="/ws", websocket=True, python_transform="response = [m for m in response if 'ERROR' in str(m) or 'WARN' in str(m)]") - HTTP subset: ha_manage_addon(slug="...", path="/flows", python_transform="response = [f['id'] for f in response]") - Array-patch (Node-RED, rename a node): ha_manage_addon( slug="a0d7b954_nodered", path="/flows", array_patch={"operations": [ {"op": "patch", "id": "abc123", "patches": {"name": "New Name"}}, ]}, ) - Array-patch (Node-RED, replace one tab's nodes atomically): ha_manage_addon( slug="a0d7b954_nodered", path="/flows", array_patch={"operations": [ {"op": "delete_where", "field": "z", "value": "tab-id"}, {"op": "add", "item": {"id": "n1", "type": "inject", "z": "tab-id", ...}}, {"op": "add", "item": {"id": "n2", "type": "function", "z": "tab-id", ...}}, ]}, request_headers={"Node-RED-Deployment-Type": "full"}, ) - Custom request headers (proxy mode): ha_manage_addon(slug="...", path="/api/state", request_headers={"Accept": "text/plain"})
Parameters
slug-(Annotated[str, Field(description="Add-on slug (e.g., '<prefix>_nodered', '<prefix>_frigate'). Slug prefixes vary by add-on repository — call ha_get_addon() to discover the actual installed slug. Required for every mode except the store-repository actions (action='add_repository'/'remove_repository'), which use 'repository' instead and take no slug.", default='')])= ""path-(Annotated[str | None, Field(description="Proxy mode: API path relative to the add-on root (e.g., '/flows', '/api/events', '/api/stats'). Required for proxy mode; mutually exclusive with config parameters.", default=None)])= nullmethod-(Annotated[str, Field(description='Proxy mode only. HTTP method: GET, POST, PUT, DELETE, PATCH. Defaults to GET.', default='GET')])= "GET"body-(Annotated[dict[str, Any] | str | None, Field(description='Proxy mode only. Request body for POST/PUT/PATCH — or, with websocket=True, the initial WebSocket message. Pass a JSON object or JSON string.', default=None)])= nulldebug-(Annotated[bool, Field(description='Proxy mode only. Include diagnostic info (request URL, headers sent, response headers). Default: false.', default=False)])= falseport-(Annotated[int | None, Field(description="Proxy mode only. Connect to this port instead of the Ingress port. Use ha_get_addon(slug='...') to find available ports.", default=None)])= nulloffset-(Annotated[int, Field(description='Proxy mode only. HTTP: skip this many items in a JSON array response. Default: 0.', default=0)])= 0limit-(Annotated[int | None, Field(description='Proxy mode only. HTTP: return at most this many items from a JSON array response.', default=None)])= nullwebsocket-(Annotated[bool, Field(description="Proxy mode only. Use WebSocket instead of HTTP — for an add-on's WebSocket API (e.g. the ESPHome dashboard's '/ws' command channel; see the docstring's ESPHome section). Sends 'body' as the initial message, collects responses. Default: false.", default=False)])= falsewait_for_close-(Annotated[bool, Field(description="Proxy mode only. WebSocket: True: wait for the server to close the stream (run-to-completion ops like an ESPHome compile/validate). False: return after the first response batch — use for a one-shot command/response or a bounded log capture on a channel that stays open (e.g. ESPHome '/ws'). Default: true.", default=True)])= truemessage_limit-(Annotated[int | None, Field(description='Proxy mode only. WebSocket: cap on messages collected from the wire, bounded by an internal safety ceiling. None = collect up to the ceiling. Lower to save tokens on noisy streams (e.g., message_limit=50 for a quick health check).', default=None)])= nullmessage_offset-(Annotated[int, Field(description='Proxy mode only. WebSocket: drop this many messages from the start of the collected list before returning. Useful for paginating past known-noisy headers. Default: 0.', default=0)])= 0summarize-(Annotated[bool, Field(description='Proxy mode only. WebSocket: when True (default), collapse runs of non-signal messages (typically YAML config dumps) into short elision markers. Set to False to return the raw stream.', default=True)])= truepython_transform-(Annotated[str | None, Field(description="Proxy mode only. Sandboxed Python expression that post-processes the response. Variable `response` is exposed — a list[dict | str] for WebSocket (parsed JSON or raw text), or dict/list/str for HTTP (parsed body). Supports in-place mutation (response.append(...)) or reassignment (response = [...]). Example: response = [m for m in response if 'ERROR' in str(m)]. Post-processing only — does not provide optimistic-locking write semantics.", default=None)])= nulloptions-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION, Field(description="Config mode: Add-on configuration values (the 'Configuration' tab in the UI).", default=None)])= nullnetwork-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION, Field(description="Config mode: Host port mappings (e.g., {'5800/tcp': 8081}).", default=None)])= nullboot-(Annotated[str | None, Field(description="Config mode: Boot strategy — 'auto' (start with HA) or 'manual'.", default=None)])= nullauto_update-(Annotated[bool | None, Field(description='Config mode: Enable or disable automatic updates for this add-on.', default=None)])= nullwatchdog-(Annotated[bool | None, Field(description='Config mode: Enable or disable Supervisor watchdog (auto-restart on crash).', default=None)])= nullarray_patch-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION, Field(description="Array-patch mode: atomically GET a JSON array endpoint, apply ordered ops, then POST the mutated array back. Requires 'path'; mutually exclusive with body / websocket / offset / limit and config params. See the docstring Examples and ha_get_skill_guide for op shapes.", default=None)])= nullrequest_headers-(Annotated[dict[str, str] | None, JSON_STRING_COERCION, Field(description="Proxy/array-patch mode: extra HTTP headers to send to the addon API. Useful for addon-specific requirements such as Node-RED's `Node-RED-Deployment-Type: full`. The proxy's internal framing (`X-Ingress-Path`, `X-Hass-Source`, `Cookie`, `Content-Type`) is layered on top, so caller-supplied values for those keys are overridden. Not valid in config or websocket mode.", default=None)])= nullaction-(Annotated[str | None, Field(description="Lifecycle mode: run a Supervisor add-on action. One of 'install', 'uninstall', 'start', 'stop', 'restart', 'rebuild', 'update'. 'install'/'update' require the add-on's repository to be registered (it appears in ha_get_addon(source='available')). Store-repository mode: 'add_repository' / 'remove_repository' register or unregister a custom add-on store repository — these use the 'repository' param instead of 'slug'. Mutually exclusive with path / config parameters / array_patch. HA OS / Supervised only.", default=None)])= nullrepository-(Annotated[str | None, Field(description="Store-repository mode only (action='add_repository' or 'remove_repository'). For add_repository: the repository URL (e.g., 'https://github.com/balloob/home-assistant-addons'). For remove_repository: the repository slug (e.g., '0f1cc410', as shown in ha_get_addon(source='available')). Required for those actions; ignored otherwise.", default=None)])= nullAreas & Floors
Description
List floors sorted by level ascending, each with their assigned areas nested, plus areas without a floor. Use for location-based reasoning where floor-to-area relationships matter, such as "which rooms are on the ground floor" or operations scoped to a level. Optionally project the response with fields= (top-level keys) or area_fields= (per-area-record keys, applied uniformly across nested, unassigned, and orphaned buckets). Floors with level=None sort alongside level 0 (ground floor). Areas without a floor assignment appear in unassigned_areas; areas whose floor_id points to a non-existent floor appear in orphaned_areas. When the ha_mcp_tools component's registries capability is available, both registries come from a single in-process snapshot, so this classification is always consistent. Without it (legacy path), the two registries are fetched via independent WebSocket calls and a registry change between reads may transiently misclassify an area.
Parameters
fields-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(default=None, description='Return only the specified top-level response keys to reduce response size (e.g. ["floors"]). None = full response (default). Available keys: success, floor_count, area_count, unassigned_count, orphaned_count, floors, unassigned_areas, orphaned_areas, message.')])= nullarea_fields-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(default=None, description='Project each area record (in floors[].areas, unassigned_areas, and orphaned_areas) to only the specified keys. E.g. ["area_id", "name"] returns slim area records. None = full records (default). Unknown keys yield empty records. Available keys: area_id, name, icon, floor_id, aliases, picture, labels.')])= nullDescription
Remove a Home Assistant area or floor. Removing an area unassigns its entities and devices (the entities and devices themselves are not removed). Removing a floor unassigns its areas. May break automations referencing the removed area/floor.
Parameters
kindrequired-(Annotated[Literal['area', 'floor'], Field(description="Which registry to delete from: 'area' or 'floor'")])idrequired-(Annotated[str, Field(description='Area ID or floor ID to delete (use ha_list_floors_areas to find IDs)')])Description
Create or update a Home Assistant area or floor. Pass kind='area' (with optional floor_id, picture) or kind='floor' (with optional level). Provide name only to create a new entry; provide id to update an existing one. Cross-kind parameters (e.g., picture under kind='floor') are rejected with VALIDATION_INVALID_PARAMETER. EXAMPLES: ha_set_area_or_floor(kind="area", name="Kitchen") ha_set_area_or_floor(kind="area", id="kitchen", floor_id="ground_floor") ha_set_area_or_floor(kind="floor", name="Basement", level=-1) ha_set_area_or_floor(kind="floor", id="ground_floor", level=0)
Parameters
kindrequired-(Annotated[Literal['area', 'floor'], Field(description="Which registry to operate on: 'area' for rooms, 'floor' for building levels")])name-(Annotated[str | None, Field(description="Name (required when creating; optional when updating, e.g., 'Living Room', 'Ground Floor')", default=None)])= nullid-(Annotated[str | None, Field(description='Existing area_id or floor_id to update (omit to create a new entry; use ha_list_floors_areas to find IDs)', default=None)])= nullfloor_id-(Annotated[str | None, Field(description="Floor assignment when kind='area' (use empty string to clear). Only valid when kind='area'.", default=None)])= nulllevel-(Annotated[int | None, Field(description="Numeric level when kind='floor' (0=ground, 1=first, -1=basement). Only valid when kind='floor'.", default=None)])= nullicon-(Annotated[str | None, Field(description="Material Design Icon (e.g., 'mdi:sofa', 'mdi:home-floor-1', empty string to remove)", default=None)])= nullaliases-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(description="Alternative names for voice assistant recognition (e.g., ['lounge'], empty list to clear)", default=None)])= nullpicture-(Annotated[str | None, Field(description="Picture URL when kind='area' (empty string to remove). Only valid when kind='area'.", default=None)])= nullAssist
Description
Manage Home Assistant Assist pipelines. Use action='list' to discover pipeline IDs, action='get' to inspect one pipeline, action='create' or action='update' to write pipeline settings, and action='set_preferred' to choose the preferred pipeline. EXAMPLES: - List pipelines: ha_manage_pipeline(action="list") - Get one pipeline: ha_manage_pipeline(action="get", pipeline_id="preferred") - Create by cloning preferred: ha_manage_pipeline( action="create", name="Local Assist", conversation_engine="conversation.local_llm", ) - Create by cloning a specific pipeline: ha_manage_pipeline( action="create", base_pipeline_id="preferred", name="Local Assist", conversation_engine="conversation.local_llm", ) - Update conversation agent and clear TTS voice: ha_manage_pipeline( action="update", pipeline_id="preferred", conversation_engine="conversation.local_llm", tts_voice="", ) - Set preferred: ha_manage_pipeline( action="set_preferred", pipeline_id="preferred", ) Empty string clears nullable STT/TTS/wake-word fields. Non-nullable fields such as name, language, conversation_language, and conversation_engine must be omitted or non-empty.
Parameters
actionrequired-(Annotated[PipelineAction, Field(description='Pipeline operation: list, get, create, update, or set_preferred.')])pipeline_id-(Annotated[str | None, Field(description='Assist pipeline ID. Required for get, update, and set_preferred.', default=None)])= nullname-(Annotated[str | None, Field(description="Pipeline display name. Required when action='create'.", default=None)])= nullconversation_engine-(Annotated[str | None, Field(description="Conversation agent entity ID or engine ID. Required when action='create'.", default=None)])= nullbase_pipeline_id-(Annotated[str | None, Field(description='Pipeline ID to clone when creating. Omit to clone the preferred pipeline. Ignored for non-create actions.', default=None)])= nullconversation_language-(Annotated[str | None, Field(description="Conversation language, usually '*'.", default=None)])= nulllanguage-(Annotated[str | None, Field(description="Pipeline language, e.g. 'en'.", default=None)])= nullstt_engine-(Annotated[str | None, Field(description='Speech-to-text engine. Pass empty string to clear.', default=None)])= nullstt_language-(Annotated[str | None, Field(description='Speech-to-text language. Pass empty string to clear.', default=None)])= nulltts_engine-(Annotated[str | None, Field(description='Text-to-speech engine. Pass empty string to clear.', default=None)])= nulltts_language-(Annotated[str | None, Field(description='Text-to-speech language. Pass empty string to clear.', default=None)])= nulltts_voice-(Annotated[str | None, Field(description='Text-to-speech voice. Pass empty string to clear.', default=None)])= nullwake_word_entity-(Annotated[str | None, Field(description='Wake-word entity ID. Pass empty string to clear.', default=None)])= nullwake_word_id-(Annotated[str | None, Field(description='Wake-word ID. Pass empty string to clear.', default=None)])= nullprefer_local_intents-(Annotated[bool | None, Field(description='Whether Home Assistant local intents should be preferred before the conversation engine.', default=None)])= nullmake_preferred-(Annotated[bool, Field(description='For create/update only, also set the resulting pipeline as preferred with an extra websocket call. Ignored for other actions.', default=False)])= falseAutomations
Description
Retrieve Home Assistant automation configuration. Returns the complete configuration including triggers, conditions, actions, and mode settings. The returned `config_hash` is stable across consecutive reads of an unchanged config — `compute_config_hash` documents the underlying contract. The returned `automation_id` is the resolved entity_id (canonical form, e.g. `automation.morning_routine`) when the registry lookup succeeds, falling back to the input `identifier` otherwise. EXAMPLES: - Get automation: ha_config_get_automation("automation.morning_routine") - Get by unique_id: ha_config_get_automation("my_unique_automation_id") For comprehensive automation documentation, use ha_get_skill_guide.
Parameters
identifierrequired-(Annotated[str, Field(description="Automation entity_id (e.g., 'automation.morning_routine') or unique_id")])Description
Delete a Home Assistant automation. The returned `automation_id` is the resolved entity_id (canonical form, e.g. `automation.morning_routine`) when the registry lookup succeeded before the delete, falling back to the input `identifier` otherwise. EXAMPLES: - Delete automation: ha_config_remove_automation("automation.old_automation") - Delete by unique_id: ha_config_remove_automation("my_unique_id") **WARNING:** Deleting an automation removes it permanently from your Home Assistant configuration.
Parameters
identifierrequired-(Annotated[str, Field(description="Automation entity_id (e.g., 'automation.old_automation') or unique_id to delete")])wait-(Annotated[bool, Field(description='Wait for automation to be fully removed before returning. Default: True.', default=True)])= trueDescription
Create or update a Home Assistant automation. MUST call ha_get_skill_guide OR refer to your locally installed skills first. PREFER NATIVE SOLUTIONS OVER TEMPLATES (read this before writing any `{{ ... }}`): Native triggers/conditions/actions are validated at config load, fail loudly, and do not bypass HA's schema. Templates fail silently at runtime and obscure intent. - `condition: numeric_state` instead of `{{ states('x') | float > N }}` - `condition: state` (with `state:` list) instead of `{{ is_state(...) }}` / `{{ states(x) in [...] }}` - `condition: time` instead of `{{ now().hour ... }}` or `{{ now().weekday() ... }}` - `condition: sun` instead of `{{ is_state('sun.sun', ...) }}` - Native `for:` field on `state`/`numeric_state` triggers and `state` conditions over `{{ now() - X.last_changed > timedelta(...) }}` duration math. - `wait_for_trigger` instead of `wait_template` - `choose` action instead of template-based service names - For one-shot date firing, use a `time` trigger plus `automation.turn_off` on a hardcoded entity_id — not `{{ now().date() ... }}`. - Hardcode `target.entity_id` literals — never `{{ this.entity_id }}`. Templates are appropriate ONLY in `data.*` fields, notification message/title, `event_data`, and `variables`. The reactive best-practice checker on this tool will surface anything in a logic position that should be native; consult the `best_practice_warnings` field on the response and fix before re-submitting. The relevant skill section is auto-embedded under `skill_content` on warnings, and the full `automation-patterns.md` + `template-guidelines.md` references ship under `skill_content` proactively by default. For comprehensive guidance beyond that, call `ha_get_skill_guide`. The returned `automation_id` is the resolved entity_id (canonical form, e.g. `automation.morning_routine`) when entity registration succeeds, falling back to the input `identifier` (update path) or the generated `unique_id` from the upsert response (fresh create when no identifier was passed). Before reaching for ``ha_config_set_automation``, consider whether a dedicated tool fits the use case better: - State snapshot of one or more entities (capture-then-replay, no trigger needed) -> ha_config_set_scene - State-derived value that recomputes when its inputs change (template sensor / binary sensor / number / select) -> ha_config_set_helper(helper_type='template') - Stateful counter / timer / schedule / boolean / etc. -> ha_config_set_helper(helper_type='counter' | 'timer' | ...) Supports two modes: full config replacement OR Python transformation. WHEN TO USE WHICH MODE: - python_transform: RECOMMENDED for edits to existing automations. Surgical updates. - config: Use for creating new automations or full restructures. IMPORTANT: python_transform requires 'identifier' and 'config_hash' from ha_config_get_automation(). PYTHON TRANSFORM EXAMPLES (operate on the fetched config, which uses HA's canonical plural root keys 'triggers'/'actions'/'conditions'): - Update action: python_transform="config['actions'][0]['data']['brightness'] = 255" - Add trigger: python_transform="config['triggers'].append({'trigger': 'state', 'entity_id': 'binary_sensor.motion', 'to': 'on'})" - Remove last action: python_transform="config['actions'].pop()" Creates a new automation (if identifier omitted) or updates existing automation with provided configuration. AUTOMATION TYPES: 1. Regular Automations - Define triggers and actions directly 2. Blueprint Automations - Use pre-built templates with customizable inputs REQUIRED FIELDS (Regular Automations): - alias: Human-readable automation name - triggers: List of triggers (time, state, event, etc.) - actions: List of actions to execute REQUIRED FIELDS (Blueprint Automations): - alias: Human-readable automation name - use_blueprint: Blueprint configuration - path: Blueprint file path (e.g., "motion_light.yaml") - input: Dictionary of input values for the blueprint OPTIONAL CONFIG FIELDS (Regular Automations): - description: Detailed description of the user's intent (RECOMMENDED: helps safely modify implementation later) - category: Category ID for organization (use ha_config_get_category to list, ha_config_set_category to create) - conditions: Additional conditions that must be met - mode: 'single' (default), 'restart', 'queued', 'parallel' - max: Maximum concurrent executions (for queued/parallel modes) - initial_state: Whether automation starts enabled (true/false) - variables: Variables for use in automation BASIC EXAMPLES: Simple time-based automation: ha_config_set_automation(config={ "alias": "Morning Lights", "description": "Turn on bedroom lights at 7 AM to help wake up", "triggers": [{"trigger": "time", "at": "07:00:00"}], "actions": [{"action": "light.turn_on", "target": {"area_id": "bedroom"}}] }) Motion-activated lighting — `for:` on the off-transition replaces action-delay: ha_config_set_automation(config={ "alias": "Motion Light", "triggers": [ {"trigger": "state", "entity_id": "binary_sensor.motion", "to": "on", "id": "motion_on"}, {"trigger": "state", "entity_id": "binary_sensor.motion", "to": "off", "for": {"minutes": 5}, "id": "motion_off"} ], "actions": [ {"choose": [ {"conditions": [ {"condition": "trigger", "id": "motion_on"}, {"condition": "sun", "after": "sunset"} ], "sequence": [{"action": "light.turn_on", "target": {"entity_id": "light.hallway"}}]}, {"conditions": [{"condition": "trigger", "id": "motion_off"}], "sequence": [{"action": "light.turn_off", "target": {"entity_id": "light.hallway"}}]} ]} ] }) Update existing automation: ha_config_set_automation( identifier="automation.morning_routine", config={ "alias": "Updated Morning Routine", "triggers": [{"trigger": "time", "at": "06:30:00"}], "actions": [ {"action": "light.turn_on", "target": {"area_id": "bedroom"}}, {"action": "climate.set_temperature", "target": {"entity_id": "climate.bedroom"}, "data": {"temperature": 22}} ] } ) BLUEPRINT AUTOMATION EXAMPLES: Create automation from blueprint: ha_config_set_automation(config={ "alias": "Motion Light Kitchen", "use_blueprint": { "path": "homeassistant/motion_light.yaml", "input": { "motion_entity": "binary_sensor.kitchen_motion", "light_target": {"entity_id": "light.kitchen"}, "no_motion_wait": 120 } } }) Update blueprint automation inputs: ha_config_set_automation( identifier="automation.motion_light_kitchen", config={ "alias": "Motion Light Kitchen", "use_blueprint": { "path": "homeassistant/motion_light.yaml", "input": { "motion_entity": "binary_sensor.kitchen_motion", "light_target": {"entity_id": "light.kitchen"}, "no_motion_wait": 300 } } } ) TRIGGER TYPES: time, time_pattern, sun, state, numeric_state, event, device, zone, template, and more CONDITION TYPES: state, numeric_state, time, sun, template, device, zone, and more ACTION TYPES: action calls, delays, wait_for_trigger, wait_template, if/then/else, choose, repeat, parallel For comprehensive automation documentation with all trigger/condition/action types and advanced examples: - Use: ha_get_skill_guide - Or visit: https://www.home-assistant.io/docs/automation/ TROUBLESHOOTING: - Use ha_get_state() to verify entity_ids exist - Use ha_search() to find correct entity_ids - IF you must use Jinja2 and have no native alternative, test it first with ha_eval_template() before embedding it in the automation config — catches syntax errors and unresolved entity_ids before they fail silently at runtime - Use ha_search(domain_filter='automation') to find existing automations
Parameters
config-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION, Field(description="Complete automation configuration with required fields: 'alias', 'triggers', 'actions'. Optional: 'description', 'conditions', 'mode', 'max', 'initial_state', 'variables'. Purpose-specific triggers/conditions (HA 2026.7+ default: 'trigger': '<domain>.<name>' with 'target'/'options') are valid config. Mutually exclusive with python_transform.", default=None)])= nullidentifier-(Annotated[str | None, Field(description='Automation entity_id or unique_id for updates. Required for python_transform. Omit to create new automation with generated unique_id.', default=None)])= nullpython_transform-(Annotated[str | None, Field(description='Python expression to transform existing automation config. Mutually exclusive with config. Requires identifier and config_hash for validation. WARNING: Expressions with infinite loops will hang the server. Examples: Simple: python_transform="config[\'actions\'][0][\'data\'][\'brightness\'] = 255" Pattern: python_transform="for a in config[\'actions\']: if a.get(\'alias\') == \'My Step\': a[\'data\'][\'value\'] = 100" \n\n' + get_security_documentation())])= nullconfig_hash-(Annotated[str | None, Field(description='Config hash from ha_config_get_automation for optimistic locking. REQUIRED for python_transform (validates automation unchanged). Optional for config updates (validates before full replacement if provided).')])= nullcategory-(Annotated[str | None, Field(description="Category ID to assign to this automation. Use ha_config_get_category(scope='automation') to list available categories, or ha_config_set_category() to create one.", default=None)])= nullwait-(Annotated[bool, Field(description='Wait for automation to be queryable before returning. Default: True. Set to False for bulk operations.', default=True)])= trueMandatoryBPS-(Annotated[bool, Field(default=True)])= trueBestPracticeKey-(BestPracticeKeyParam)= nullBlueprints
Description
Get blueprint information - list all blueprints or get details for a specific one. Without a path: Lists all installed blueprints for the specified domain. With a path: Returns the blueprint's metadata and input definitions. The full body (triggers/conditions/actions for automations, sequence for scripts) is included under `config` ONLY when the ha_mcp_tools custom component is installed — core's blueprint API exposes metadata alone, so without the component the body cannot be read. EXAMPLES: - List all automation blueprints: ha_get_blueprint(domain="automation") - List script blueprints: ha_get_blueprint(domain="script") - Get specific blueprint: ha_get_blueprint(path="homeassistant/motion_light.yaml", domain="automation") RETURNS (when listing): - List of blueprints with path, name, and domain information - Count of blueprints found RETURNS (when getting specific blueprint): - Blueprint metadata (name, description, author, source_url) - Input definitions with selectors and defaults - `config`: the full parsed blueprint body (only with the ha_mcp_tools component; `!input` substitution points appear as `{"__input__": name}`)
Parameters
path-(Annotated[str | None, Field(description="Blueprint path to get details for (e.g., 'homeassistant/motion_light.yaml'). If omitted, lists all blueprints in the domain.", default=None)])= nulldomain-(Annotated[str, Field(description="Blueprint domain: 'automation' or 'script'", default='automation')])= "automation"Description
Import a blueprint from a URL. Imports a blueprint from GitHub, Home Assistant Community forums, or any direct URL to a blueprint YAML file. Set overwrite=true to re-import a blueprint that is already installed (equivalent to the UI's "Re-import blueprint" action) - Home Assistant then reloads all automations/scripts that use it. EXAMPLES: - Import from GitHub: ha_import_blueprint("https://github.com/user/repo/blob/main/blueprint.yaml") - Import from HA Community: ha_import_blueprint("https://community.home-assistant.io/t/motion-light/123456") - Import direct YAML: ha_import_blueprint("https://example.com/my-blueprint.yaml") - Re-import an installed blueprint: ha_import_blueprint("https://example.com/my-blueprint.yaml", overwrite=True) SUPPORTED SOURCES: - GitHub repository URLs (will be converted to raw URLs) - Home Assistant Community forum posts with blueprint code - Direct URLs to YAML blueprint files RETURNS: - Import result with the blueprint path where it was saved - Blueprint metadata (name, domain, description) - overrides_existing: true when an installed blueprint was overwritten - Error details if import fails
Parameters
urlrequired-(Annotated[str, Field(description='URL to import blueprint from (GitHub, Home Assistant Community, or direct YAML URL)')])overwrite-(Annotated[bool, Field(description='Overwrite the blueprint if it is already installed (re-import). Home Assistant reloads all automations/scripts using the blueprint.', default=False)])= falseCalendar
Description
Retrieve calendar events from a calendar entity. Retrieves calendar events within a specified time range. **Parameters:** - entity_id: Calendar entity ID (e.g., 'calendar.family') - start: Start datetime in ISO format (default: now) - end: End datetime in ISO format (default: 7 days from start) - max_results: Maximum number of events to return (default: 20) **Example Usage:** ```python # Get events for the next week events = ha_config_get_calendar_events("calendar.family") # Get events for a specific date range events = ha_config_get_calendar_events( "calendar.work", start="2024-01-01T00:00:00", end="2024-01-31T23:59:59" ) ``` **Note:** To find calendar entities, use ha_search(query='calendar', domain_filter='calendar') **Returns:** - List of calendar events with summary, start, end, description, location
Parameters
entity_idrequired-(Annotated[str, Field(description="Calendar entity ID (e.g., 'calendar.family')")])start-(Annotated[str | None, Field(description='Start datetime in ISO format (default: now)', default=None)])= nullend-(Annotated[str | None, Field(description='End datetime in ISO format (default: 7 days from start)', default=None)])= nullmax_results-(Annotated[int, Field(description='Maximum number of events to return', default=20)])= 20Description
Delete an event from a calendar. Deletes a calendar event via the WebSocket ``calendar/event/delete`` command. HA's calendar component only registers ``create_event`` and ``get_events`` as REST services — delete and update live on the WebSocket API only. **Parameters:** - entity_id: Calendar entity ID (e.g., 'calendar.family') - uid: Unique identifier of the event to delete - recurrence_id: Optional recurrence ID for recurring events - recurrence_range: Optional recurrence range ('THIS_AND_FUTURE' to delete this and future occurrences) **Example Usage:** ```python # Delete a single event result = ha_config_remove_calendar_event( "calendar.family", uid="event-12345" ) # Delete a recurring event instance and future occurrences result = ha_config_remove_calendar_event( "calendar.work", uid="recurring-event-67890", recurrence_id="20240115T100000", recurrence_range="THIS_AND_FUTURE" ) ``` **Note:** To get the event UID, first use ha_config_get_calendar_events() to list events. The UID is returned in each event's data. **Returns:** - Success status and deletion confirmation
Parameters
entity_idrequired-(Annotated[str, Field(description="Calendar entity ID (e.g., 'calendar.family')")])uidrequired-(Annotated[str, Field(description='Unique identifier of the event to delete')])recurrence_id-(Annotated[str | None, Field(description='Optional recurrence ID for recurring events', default=None)])= nullrecurrence_range-(Annotated[str | None, Field(description="Optional recurrence range ('THIS_AND_FUTURE' to delete this and future occurrences)", default=None)])= nullDescription
Create a new event in a calendar. Creates a one-off event via the calendar.create_event service, or a recurring series via the WebSocket ``calendar/event/create`` command when ``rrule`` is provided (the REST service schema does not accept recurrence rules). **When NOT to use:** - To retrieve calendar events, use ``ha_config_get_calendar_events``. - To delete an event, use ``ha_config_remove_calendar_event``. **Example Usage:** ```python # Create a simple event result = ha_config_set_calendar_event( "calendar.family", summary="Doctor appointment", start="2024-01-15T14:00:00", end="2024-01-15T15:00:00" ) # Create a recurring event (every Monday, 10 occurrences) result = ha_config_set_calendar_event( "calendar.work", summary="Team meeting", start="2024-01-15T10:00:00", end="2024-01-15T11:00:00", rrule="FREQ=WEEKLY;BYDAY=MO;COUNT=10" ) # Create an all-day event (date-only, no time component). The end # date is EXCLUSIVE, so this spans 2026-07-04 through 2026-07-10. result = ha_config_set_calendar_event( "calendar.family", summary="Vacation", start="2026-07-04", end="2026-07-11" ) ``` **Note:** Passing date-only values (``YYYY-MM-DD``) for both ``start`` and ``end`` creates an all-day event; passing full ISO datetimes creates a timed event. The two forms cannot be mixed — a date-only ``start`` with a datetime ``end`` (or vice versa) is rejected. Because the all-day ``end`` date is exclusive, a single-day all-day event must set ``end`` to ``start + 1 day``. Not every calendar integration supports event creation; recurring events additionally require the integration to support recurrence (the built-in Local Calendar does). **Returns:** - Success status and event details
Parameters
entity_idrequired-(Annotated[str, Field(description="Calendar entity ID (e.g., 'calendar.family')")])summaryrequired-(Annotated[str, Field(description='Event title/summary')])startrequired-(Annotated[str, Field(description='Event start date or datetime in ISO format')])endrequired-(Annotated[str, Field(description='Event end date or datetime in ISO format. For all-day events (date-only) the end date is exclusive; a single-day all-day event needs end = start + 1 day.')])description-(Annotated[str | None, Field(description='Optional event description', default=None)])= nulllocation-(Annotated[str | None, Field(description='Optional event location', default=None)])= nullrrule-(Annotated[str | None, Field(description="Optional RFC 5545 recurrence rule, without 'RRULE:' prefix (e.g., 'FREQ=WEEKLY;BYDAY=MO' or 'FREQ=MONTHLY;BYDAY=3SA'). Creates a recurring event series.", default=None)])= nullCamera
Description
Retrieve a snapshot image from a Home Assistant camera entity. This tool fetches the current camera image and returns it directly for visual analysis. Use this when you need to see what a camera is currently viewing. **Parameters:** - entity_id: Camera entity ID (e.g., 'camera.front_door', 'camera.living_room') - width: Optional width to resize the image (reduces token usage for large images) - height: Optional height to resize the image **Use Cases:** - Security checks: "Is someone at the front door?" - Pet monitoring: "Is my dog still on the couch?" - Delivery verification: "Did my package get delivered?" - Visual confirmation: "Did the garage door actually close?" - Incident investigation: "What triggered the motion sensor?" **Example Usage:** ```python # Get current snapshot from front door camera ha_get_camera_image(entity_id="camera.front_door") # Get resized image to reduce token usage ha_get_camera_image(entity_id="camera.backyard", width=640, height=480) ``` **Notes:** - Only cameras exposed to Home Assistant are accessible - The existing HA authentication/authorization applies - Images are returned in their native format (JPEG, PNG, or GIF) - Use width/height parameters for large high-resolution cameras to reduce token usage when full resolution is not needed **Related Services:** - camera.snapshot: Save snapshot to file on HA server - camera.turn_on/turn_off: Control camera power - camera.enable_motion_detection: Enable motion detection
Parameters
entity_idrequired-(str)width-(int | None)= nullheight-(int | None)= nullDashboard
Description
Get rendered images of a Home Assistant Lovelace dashboard view. When not to use: while reading or writing dashboard configuration, use ha_config_get_dashboard(include_screenshot=True) or ha_config_set_dashboard(return_screenshot=True) for a single workflow. Use it for repeatable visual checks, including ordered mobile, tablet, and desktop captures. Puppet reports image bytes but does not confirm that the frontend accepted a requested theme or language; structured metadata therefore records the values sent to the engine. To change the Puppet engine add-on itself (keep_browser_open, restart), use ha_manage_addon.
Parameters
dashboard_path-(Annotated[str | None, Field(description="Legacy Lovelace frontend path to render, e.g. 'lovelace/0' (default dashboard, first view), 'lovelace-home/kitchen', or 'my-dashboard'. Leading slash optional. Prefer dashboard_url_path + view_path for a stable named view. Mutually exclusive with dashboard_url_path.")])= nulldashboard_url_path-(Annotated[str | None, Field(description="Stable dashboard URL path, e.g. 'lovelace-home' or 'default'. Use with view_path instead of dashboard_path.")])= nullview_path-(Annotated[str | None, Field(description='Stable Lovelace views[].path value to render. Requires dashboard_url_path.')])= nullwidth-(Annotated[int, Field(description='Viewport width in px.', ge=64, le=4096)])height-(Annotated[int | Literal['auto'], Field(description="Viewport height in px (64-4096), or 'auto' for content height. full_page=True is a compatibility alias for 'auto'.")])viewport_presets-(Annotated[list[ViewportPreset] | None, JSON_STRING_COERCION, Field(description='Render one or more named responsive viewports in this order: mobile (390x844), tablet (768x1024), desktop (1280x800). Overrides width/height.', min_length=1, max_length=3)])= nullorientation-(Annotated[Orientation | None, Field(description='Optional responsive orientation. Swaps viewport dimensions when needed; this does not rotate final pixels.')])= nullzoom-(Annotated[float, Field(description='Page zoom factor (1.0 = 100%).', ge=0.1, le=5.0)])= 1wait_ms-(Annotated[int, Field(description='Extra render-settle time (ms) after the dashboard reports loaded. Raise it if a chart card (ApexCharts, mini-graph, history-graph) comes back blank.', ge=0, le=30000)])full_page-(Annotated[bool, Field(description=f"{FULL_PAGE_PARAM_DESC[:1].upper()}{FULL_PAGE_PARAM_DESC[1:]}. Uses Puppet's native auto-height capture (currently capped at 4000 px); raise wait_ms for lazy cards.")])= falsetheme-(Annotated[str | None, Field(description="Installed Home Assistant frontend theme name, applied to this render. The engine user's saved theme preference is restored after the capture (best effort).")])= nulldark_mode-(Annotated[bool, Field(description="Render the requested theme in dark mode, applied to this render. The engine user's saved theme preference is restored after the capture (best effort).")])= falselanguage-(Annotated[str | None, Field(description="Frontend language code, e.g. 'en' or 'de'.")])= nullimage_format-(Annotated[ScreenshotFormat, Field(description='Image format: png, jpeg, webp, or bmp.')])= "png"render_timeout_seconds-(Annotated[float, Field(description='HTTP render timeout in seconds.', ge=1, le=300)])Dashboards
Description
Delete a storage-mode dashboard completely. WARNING: This permanently deletes the dashboard and all its configuration. Cannot be undone. Does not work on YAML-mode dashboards. Accepts either the URL path or the internal dashboard ID. HA internal IDs may differ from url_path (e.g. hyphens → underscores); the tool resolves either form to the actual registry ID before deletion. EXAMPLES: - Delete dashboard: ha_config_delete_dashboard("mobile-dashboard") Note: The default dashboard cannot be deleted via this method.
Parameters
url_pathrequired-(Annotated[str, Field(description="Dashboard URL path or internal ID to delete (e.g., 'my-dashboard' or 'my_dashboard'). Both forms are accepted.")])Description
Delete a dashboard resource. Removes a resource from Home Assistant. The resource will no longer be loaded on dashboards. WARNING: Deleting a resource used by custom cards in your dashboards will cause those cards to fail to load. EXAMPLES: ha_config_delete_dashboard_resource(resource_id="abc123") Note: Use ha_config_list_dashboard_resources() to find resource IDs before deleting. Ensure no dashboards depend on the resource.
Parameters
resource_idrequired-(Annotated[str, Field(description='Resource ID to delete. Get from ha_config_list_dashboard_resources()')])Description
Get dashboard info - list all dashboards, get config, or search for cards. MODE 1 — List: list_only=True Lists every dashboard's metadata (url_path, title, icon), storage and YAML alike (metadata only — bodies are never included here). MODE 2 — Search: any of entity_id / card_type / heading provided Finds cards, badges, and header cards matching the criteria, including cards nested inside stacks, grids, conditional cards, button-card custom_fields, and state-switch states. Each match carries a python_path and a jq_path that locate the card for nested as well as top-level cards. The python_path is a Python subscript chain to be appended after `config` — e.g. python_transform=f'config{m["python_path"]}["icon"] = "mdi:x"' (it is NOT valid on its own without the `config` prefix). jq_path is the same location in jq dot-notation. Multiple criteria are AND-ed. Always fetches fresh config (force=True). Search covers cards/card/custom_fields/states containers up to a depth bound; if the dashboard carries a non-traversed child-bearing shape (e.g. picture-elements `elements`), the result carries a `warnings` entry naming where, so its hidden content is not mistaken for absent. Strategy dashboards are not searchable (no explicit cards). MODE 3 — Get: Active when list_only=False and no search parameters are provided. Returns the full Lovelace dashboard config, defaulting to the main dashboard if url_path is omitted. Pass view_path=<views[].path> to return ONLY that view: the response then carries `view` + `view_index` instead of `config`, keeping the payload small on multi-view dashboards. `config_hash` still covers the FULL config, so a follow-up ha_config_set_dashboard(python_transform=...) addressing config['views'][view_index] validates unchanged. An unknown view_path errors and lists the available view paths. MODE 4 — Search all: mode="search" with query=<entity_id or text> Answers "which dashboards contain this entity/card" by walking every storage-mode dashboard's views/cards/sections for the query substring. Each match names the url_path, view, card_path, card_type, and the matched field/value. Takes precedence over the other modes (list_only / entity_id / card_type / heading are ignored when mode="search"). YAML-mode dashboards are never searched on either path — the component walk skips them in-process and the component-less legacy walk skips any row tagged mode="yaml" — because HA resolves `!secret` when loading a YAML Lovelace config, so searching one could surface resolved secrets. On installs without the ha_mcp_tools component, the default (unnamed) dashboard is also not searched — only dashboards with a url_path are. Return a stable `config_hash` (Get and Search modes only; not present in list_only mode) across consecutive reads of an unchanged config — `compute_config_hash` documents the underlying contract. EXAMPLES: - List all dashboards: ha_config_get_dashboard(list_only=True) - Get default dashboard: ha_config_get_dashboard(url_path="default") - Get custom dashboard: ha_config_get_dashboard(url_path="lovelace-mobile") - Get one view only: ha_config_get_dashboard(url_path="lovelace-mobile", view_path="office") - Force reload: ha_config_get_dashboard(url_path="lovelace-home", force_reload=True) - Find cards by entity: ha_config_get_dashboard(url_path="my-dash", entity_id="light.living_room") - Find by wildcard: ha_config_get_dashboard(url_path="my-dash", entity_id="sensor.temperature_*") - Find by type: ha_config_get_dashboard(url_path="my-dash", card_type="tile") - Find heading: ha_config_get_dashboard(url_path="my-dash", heading="Climate", card_type="heading") SEARCH WORKFLOW EXAMPLE: 1. find = ha_config_get_dashboard(url_path="my-dash", entity_id="light.bedroom") 2. ha_config_set_dashboard( url_path="my-dash", config_hash=find["config_hash"], python_transform=f'config{find["matches"][0]["python_path"]}["icon"] = "mdi:lamp"' ) Note: YAML-mode dashboards (defined in configuration.yaml) are not included in list.
Parameters
url_path-(Annotated[str | None, Field(description="Dashboard URL path (e.g., 'lovelace-home'). Use 'default' for default dashboard. If omitted with list_only=True, lists all dashboards.")])= nulllist_only-(Annotated[bool, Field(description='If True, list all dashboards instead of getting config. When True, url_path is ignored.')])= falseforce_reload-(Annotated[bool, Field(description='Force reload from storage (bypass cache). Not applicable in search mode (search always uses force=True for fresh results).')])= falseentity_id-(Annotated[str | None, Field(description="Find cards by entity ID. Supports wildcards, e.g. 'sensor.temperature_*'. Matches cards with this entity in 'entity' or 'entities' field, view-level badges, and header cards. When provided, activates search mode (returns matches, not full config).")])= nullcard_type-(Annotated[str | None, Field(description="Find cards by type, e.g. 'tile', 'button', 'heading'. When provided, activates search mode.")])= nullheading-(Annotated[str | None, Field(description='Find cards by heading/title text (case-insensitive partial match). When provided, activates search mode.')])= nullinclude_config-(Annotated[bool, Field(description="In search mode: include each matched card's own configuration object in results (increases output size). Note that a matched container card's config contains its descendants, which are themselves separate matches with their own config, so deeply-nested stacks multiply the payload — keep the default (False) unless you need the bodies. Does not affect whether the full dashboard config is returned — search mode always returns matches only, not the full dashboard. Config bodies are surfaced only for dashboards provably in storage mode; for a YAML or unconfirmed dashboard the bodies are withheld (they may carry resolved !secret values) and the response says so, with match locations still reported. Ignored outside search mode.")])= falseinclude_screenshot-(Annotated[bool, Field(description="Get mode only: also return rendered image(s) of the dashboard for visual verification. Requires the 'dashboard screenshot' beta feature + engine add-on/sidecar. If the feature is disabled the config is returned with a warning; if the engine is configured but the render fails, the call errors (the screenshot is the requested payload). Ignored in list/search mode.")])= falseview_path-(Annotated[str | None, Field(description="Get mode: return ONLY the view whose Lovelace views[].path matches (response carries 'view' + 'view_index' instead of the full 'config') — use this to keep multi-view dashboards from blowing up the response when you only need one view. Does not require any beta feature. With include_screenshot, also selects the view to render. Ignored in list/search mode. Omit for the full config.")])= nullmode-(Annotated[Literal['search'] | None, Field(description="Set to 'search' for a CROSS-dashboard search: which dashboards contain a given entity_id or text (requires query). Leave unset for the default list/get/single-dashboard-search behavior selected by list_only / entity_id / card_type / heading.")])= nullquery-(Annotated[str | None, Field(description="With mode='search': the entity_id or substring to find across all storage-mode dashboards. Ignored otherwise.")])= nullDescription
List Lovelace dashboard resources (custom cards, themes, CSS/JS). Returns one page of registered resources; `total_count` and `has_more` report the full set. For inline resources (created with ha_config_set_dashboard_resource(content=...)), shows a preview of the content instead of the full encoded URL to save tokens. `inline_count` and `by_type` summarise every resource, not just this page. Args: include_content: If True, includes full decoded content for inline resources in "_content" field. Default False (150-char preview only). Resource types: - module: ES6 JavaScript modules (modern custom cards) - js: Legacy JavaScript files - css: CSS stylesheets Each resource has a unique ID for update/delete operations. EXAMPLES: - First page of resources: ha_config_list_dashboard_resources() - Next page: ha_config_list_dashboard_resources(offset=100) - List with full content: ha_config_list_dashboard_resources(include_content=True) Note: Home Assistant 2026.6+ exposes resource management in the UI by default, and API access works regardless of UI availability.
Parameters
include_content-(Annotated[bool, Field(description='Include full decoded content for inline resources. Default False to save tokens (shows 150-char preview instead).')])= falselimit-(Annotated[int, Field(default=100, ge=1, le=500, description='Max resources to return per page (default: 100)')])= 100offset-(Annotated[int, Field(default=0, ge=0, description='Number of resources to skip for pagination (default: 0)')])= 0Description
Create or update a Home Assistant dashboard. MUST call ha_get_skill_guide OR refer to your locally installed skills first. Creates a new dashboard or updates an existing one with the provided configuration. Supports two modes: full config replacement OR Python transformation. Use 'default' or 'lovelace' to target the built-in default dashboard. New dashboards require a hyphenated url_path (e.g., 'my-dashboard'). WHEN TO USE WHICH MODE: - python_transform: RECOMMENDED for edits. Surgical/pattern-based updates, works on all platforms. - config: New dashboards only, or full restructure. Replaces everything. IMPORTANT: After delete/add operations, indices shift! Subsequent python_transform calls must use fresh config_hash from ha_config_get_dashboard() to get updated structure. Chain multiple ops in ONE expression when possible. TIP: Use ha_config_get_dashboard(entity_id=...) to get the path for any card. PYTHON TRANSFORM EXAMPLES (RECOMMENDED): - Update card icon: 'config["views"][0]["cards"][0]["icon"] = "mdi:thermometer"' - Add card: 'config["views"][0]["cards"].append({"type": "button", "entity": "light.bedroom"})' - Delete card: 'del config["views"][0]["cards"][2]' - Pattern-based update: 'for card in config["views"][0]["cards"]: if "light" in card.get("entity", ""): card["icon"] = "mdi:lightbulb"' - Multi-operation: 'config["views"][0]["cards"][0]["icon"] = "mdi:a"; config["views"][0]["cards"][1]["icon"] = "mdi:b"' MODERN DASHBOARD BEST PRACTICES: - Use "sections" view type (default) with grid-based layouts - Use "tile" cards as primary card type (replaces legacy entity/light/climate cards) - Use "grid" cards for multi-column layouts within sections - Create multiple views with navigation paths (avoid single-view endless scrolling) - Use "area" cards with navigation for hierarchical organization DISCOVERING ENTITY IDs FOR DASHBOARDS: Do NOT guess entity IDs - use these tools to find exact entity IDs: 1. ha_get_overview(include_entity_id=True) - Get all entities organized by domain/area 2. ha_search(query, domain_filter, area_filter, search_types) - Find entities and config-body references in one call If unsure about entity IDs, ALWAYS use one of these tools first. DASHBOARD DOCUMENTATION: - dashboard-guide.md and dashboard-cards.md ship in this response under ``skill_content`` by default — layout patterns, card-type taxonomy, and worked examples. - ha_get_skill_guide — deeper card-type and configuration guidance. EXAMPLES: Create empty dashboard: ha_config_set_dashboard( url_path="mobile-dashboard", title="Mobile View", icon="mdi:cellphone" ) Create dashboard with modern sections view: ha_config_set_dashboard( url_path="home-dashboard", title="Home Overview", config={ "views": [{ "title": "Home", "type": "sections", "sections": [{ "title": "Climate", "cards": [{ "type": "tile", "entity": "climate.living_room", "features": [{"type": "target-temperature"}] }] }] }] } ) Create strategy-based dashboard (auto-generated): ha_config_set_dashboard( url_path="my-home", title="My Home", config={ "strategy": { "type": "home", "favorite_entities": ["light.bedroom"] } } ) Note: Strategy dashboards cannot be converted to custom dashboards via this tool. Use the "Take Control" feature in the Home Assistant interface to convert them. Update existing dashboard config: ha_config_set_dashboard( url_path="existing-dashboard", config={ "views": [{ "title": "Updated View", "type": "sections", "sections": [{ "cards": [{"type": "markdown", "content": "Updated!"}] }] }] } ) Note: When updating an existing dashboard, title/icon/require_admin/show_in_sidebar are also updated if explicitly provided alongside (or instead of) a config change. STORAGE-MODE vs YAML-MODE DASHBOARDS: This tool only manages storage-mode dashboards (created via UI/API and stored in Home Assistant's storage backend). It does NOT touch YAML-defined dashboards. Two distinct YAML cases exist and this tool covers neither: - "YAML-mode" dashboards: written in their own .yaml file referenced from configuration.yaml under ``lovelace: dashboards:``. The dashboard itself lives in a separate YAML file but its registration is in configuration.yaml. - Dashboards inlined directly in ``configuration.yaml`` under the ``lovelace:`` key (legacy single-dashboard mode). For either YAML case, edit the dashboard's .yaml file directly. ``ha_config_set_yaml`` can update the ``lovelace:`` registration entry in configuration.yaml but does NOT touch the dashboard body in the referenced .yaml file.
Parameters
url_pathrequired-(Annotated[str, Field(description="Dashboard URL path (e.g., 'my-dashboard'). Use 'default' or 'lovelace' for the default dashboard. New dashboards must use a hyphenated path.")])config-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION, Field(description='Dashboard configuration with views and cards. Omit or set to None to create dashboard without initial config. Mutually exclusive with python_transform.')])= nullpython_transform-(Annotated[str | None, Field(description='Python expression to transform existing dashboard config. Mutually exclusive with config. Requires config_hash for validation. See PYTHON TRANSFORM SECURITY below for allowed operations. Examples: Simple: python_transform="config[\'views\'][0][\'cards\'][0][\'icon\'] = \'mdi:lamp\'" Pattern: python_transform="for card in config[\'views\'][0][\'cards\']: if \'light\' in card.get(\'entity\', \'\'): card[\'icon\'] = \'mdi:lightbulb\'" Multi-op: python_transform="config[\'views\'][0][\'cards\'][0][\'icon\'] = \'mdi:lamp\'; del config[\'views\'][0][\'cards\'][2]" \n\n' + get_security_documentation())])= nullconfig_hash-(Annotated[str | None, Field(description='Config hash from ha_config_get_dashboard for optimistic locking. REQUIRED for python_transform (validates dashboard unchanged). Optional for config (validates before full replacement if provided).')])= nulltitle-(Annotated[str | None, Field(description='Dashboard display name shown in sidebar')])= nullicon-(Annotated[str | None, Field(description="MDI icon name (e.g., 'mdi:home', 'mdi:cellphone'). Defaults to 'mdi:view-dashboard'")])= nullrequire_admin-(Annotated[bool | None, Field(description='Restrict dashboard to admin users only. For existing dashboards, only updated when explicitly provided.')])= nullshow_in_sidebar-(Annotated[bool | None, Field(description='Show dashboard in sidebar navigation. For existing dashboards, only updated when explicitly provided.')])= nullMandatoryBPS-(Annotated[bool, Field(default=True)])= trueBestPracticeKey-(BestPracticeKeyParam)= nullreturn_screenshot-(Annotated[bool, Field(description="After writing, also return rendered image(s) of the dashboard so you can see what it looks like in a single call (the dashboard creation/iteration loop). Requires the 'dashboard screenshot' beta feature + engine add-on/sidecar; if unavailable, the write result is returned with a warning.")])= falseview_path-(Annotated[str | None, Field(description='With return_screenshot: stable Lovelace views[].path to render.')])= nullDescription
Create or update a dashboard resource (inline code or external URL). Provide exactly one of: - content: Inline JavaScript or CSS code (embedded in the resource URL as a data: URI — no file storage or external hosting involved) - url: External resource URL (/local/, /hacsfiles/, or https://...) INLINE MODE (content=): - Custom card code written inline - CSS styling for dashboards - Self-contained files up to ~128KB - URLs are deterministic (same content = same URL) - Content must be self-contained: a data: URI has no base URL, so relative imports inside a module and relative url() references inside CSS cannot resolve (use fully-qualified URLs instead) - If Home Assistant is behind a reverse proxy that injects a Content-Security-Policy without 'data:' in script-src/style-src, the browser blocks these resources: this call still succeeds and the card simply never renders. Register the code as a file and use url='/local/...' on such a deployment. (HA itself ships no CSP.) - Supports 'module' and 'css' types only (not 'js') URL MODE (url=): - Files in /config/www/ directory (/local/...) - HACS-installed cards (/hacsfiles/...) - External CDN resources (https://...) - Supports all types: 'module', 'js', 'css' RESOURCE TYPES: - module: ES6 JavaScript modules (recommended for custom cards) - js: Legacy JavaScript files (older custom cards, url mode only) - css: CSS stylesheets (themes, global styles) EXAMPLES: Inline custom card: ha_config_set_dashboard_resource( content=""" class MyCard extends HTMLElement { setConfig(config) { this.config = config; } set hass(hass) { this.innerHTML = `<ha-card>Hello ${hass.states[this.config.entity]?.state}</ha-card>`; } } customElements.define('my-card', MyCard); """, resource_type="module" ) Add custom card from www/ directory: ha_config_set_dashboard_resource( url="/local/my-custom-card.js", resource_type="module" ) Add HACS card (after installing via ha_manage_hacs(action='download')): ha_config_set_dashboard_resource( url="/hacsfiles/lovelace-mushroom/mushroom.js", resource_type="module" ) Update existing resource: ha_config_set_dashboard_resource( url="/local/my-card-v2.js", resource_type="module", resource_id="abc123" ) Note: After adding a resource, clear browser cache or hard refresh (Ctrl+Shift+R) to load changes.
Parameters
content-(Annotated[str | None, Field(description="JavaScript or CSS code to host inline (max ~128KB). The code is embedded directly in the resource URL as a data: URI - no file storage or external hosting involved. Mutually exclusive with url. Supports 'module' and 'css' types only.")])= nullurl-(Annotated[str | None, Field(description='URL of the resource. Can be: /local/file.js (www/ directory), /hacsfiles/component/file.js (HACS), https://cdn.example.com/card.js (external). Mutually exclusive with content.')])= nullresource_type-(Annotated[Literal['module', 'js', 'css'], Field(description="Resource type: 'module' for ES6 modules (modern cards, default), 'js' for legacy JavaScript (url mode only), 'css' for stylesheets")])= "module"resource_id-(Annotated[str | None, Field(description='Resource ID to update. If omitted, creates a new resource. Get IDs from ha_config_list_dashboard_resources()')])= nullDeveloper
Description
Manage the running ha-mcp server itself (developer mode). When NOT to use: to restart Home Assistant use ha_restart; to update HA add-ons or HACS packages use ha_manage_addon / ha_manage_hacs. When to use: development/testing workflows — inspecting how this server is deployed, switching the in-process (custom component) server to another release channel or an arbitrary pip spec such as a PR tarball, and restarting the server so config or code changes take effect. Caveats: update_source changes ONLY the ha_mcp_tools custom component's separate in-process server entry — it never updates the add-on, Docker, standalone, or PyPI server that may be serving this connection (update those via ha_manage_addon / docker pull / pip). In embedded mode that entry IS this server, so the update self-interrupts; elsewhere this connection is untouched and keeps its current version. Success means the entry's options were applied — the component then reinstalls in the background, which can take minutes and can still fail (check HA logs). update_source requires the component's in-process server entry to exist. restart interrupts this MCP connection in embedded and add-on deployments (the reply arrives just before the server goes down) and supports those two deployments only (standalone processes must be restarted externally). list_pending/approve/deny are exempt from policy gating (gating queue management would deadlock approvals) — and since gated-call errors include the approval token, an agent can self-approve its own gated calls while dev mode is on. Dev mode is a trusted-operator feature; leave it off otherwise. EXAMPLES: ha_dev_manage_server("info") ha_dev_manage_server("update_source", channel="dev") ha_dev_manage_server("update_source", pip_spec="https://github.com/homeassistant-ai/ha-mcp/archive/refs/pull/1234/head.tar.gz") ha_dev_manage_server("restart") ha_dev_manage_server("list_pending") ha_dev_manage_server("approve", token="abc123")
Parameters
actionrequired-(Annotated[Literal['info', 'update_source', 'restart', 'list_pending', 'approve', 'deny'], Field(description="info: deployment/version report; update_source: point the ha_mcp_tools component's separate in-process server at a channel or pip spec and reinstall it (never changes the server serving this connection, unless embedded); restart: restart this server; list_pending: list tool calls blocked on a security-policy approval; approve / deny: decide one blocked call by token")])channel-(Annotated[str | None, Field(default=None, description="Release channel for update_source: 'stable' or 'dev'")])= nullpip_spec-(Annotated[str | None, Field(default=None, description='Explicit pip requirement for update_source — a version pin (ha-mcp==7.9.0) or a GitHub tarball URL such as https://github.com/homeassistant-ai/ha-mcp/archive/refs/pull/<PR>/head.tar.gz. Empty string clears the override.')])= nulltoken-(Annotated[str | None, Field(default=None, description='Approval token (required for approve/deny)')])= nullDescription
Manage ha-mcp server settings and the Tools/Policies/Backups surfaces (developer mode). Drives everything the web settings UI can change: the Server Settings matrix (list/set/reset), the Tools tab (enable/disable/pin, LLM-API exposure, and the per-tool security gate), the Tool Security Policies editor (get_policy/set_policy), and the auto-backup config (get_backup_config/set_backup_config). Use ha_dev_manage_server for the live approval queue and to restart. When NOT to use: for HA entity/automation configuration use the ha_config_* tools. Caveats: enable/disable/pin and most server settings take effect only after a restart (ha_dev_manage_server action="restart"); LLM-API exposure, security gates, and policy edits apply live. Env-pinned settings/tools are read-only until the env var is unset. These actions can flip security-sensitive state — treat with the same care as editing the web UI. EXAMPLES: ha_dev_manage_settings("list_tools") ha_dev_manage_settings("set_tool", tool="ha_write_file", state="disabled") ha_dev_manage_settings("set_tool", tool="ha_call_service", gated=True) ha_dev_manage_settings("get_policy") ha_dev_manage_settings("set_backup_config", backup={"enable_auto_backup": False})
Parameters
actionrequired-(Annotated[Literal['list', 'set', 'reset', 'list_tools', 'set_tool', 'get_policy', 'set_policy', 'get_backup_config', 'set_backup_config'], Field(description='Server-settings matrix: list / set / reset. Tools tab: list_tools / set_tool (enable-disable-pin, LLM-API, security gate). Security policies: get_policy / set_policy. Auto-backup config: get_backup_config / set_backup_config.')])setting-(Annotated[str | None, Field(default=None, description='Setting name (required for set/reset)')])= nullvalue-(Annotated[bool | int | float | str | None, Field(default=None, description='New value (required for set)')])= nulltool-(Annotated[str | None, Field(default=None, description='Tool name (required for set_tool)')])= nullstate-(Annotated[Literal['enabled', 'disabled', 'pinned'] | None, Field(default=None, description='set_tool: enable, disable, or pin the tool')])= nullllm_api-(Annotated[bool | None, Field(default=None, description='set_tool: expose the tool to HA conversation agents (effective only on the embedded custom-component server)')])= nullgated-(Annotated[bool | None, Field(default=None, description='set_tool: require user approval before every call to this tool (adds/removes an unconditional security-policy rule)')])= nullpolicy-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION, Field(default=None, description='set_policy: the full policy object {wait_seconds, approval_ttl_minutes, rules, version}')])= nullexpected_version-(Annotated[int | None, Field(default=None, description="set_policy: the version from your last get_policy, for optimistic-concurrency safety (else the policy's own version field is used)")])= nullbackup-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION, Field(default=None, description='set_backup_config: {field: value} of auto-backup settings to change (see get_backup_config for field names)')])= nullDevice Registry
Description
Get device information with pagination, including Zigbee (ZHA/Z2M) and Z-Wave JS devices. Without device_id/entity_id: Lists devices with optional filters and pagination. With device_id or entity_id: Returns full detail for that specific device. **List devices (paginated):** - First page: ha_get_device() - Next page: ha_get_device(offset=50) - By area: ha_get_device(area_id="living_room") - By integration: ha_get_device(integration="zigbee2mqtt") - Full details in list: ha_get_device(detail_level="full", limit=10) **Single device lookup (always full detail):** - By device_id: ha_get_device(device_id="abc123") - By entity_id: ha_get_device(entity_id="light.living_room") **Zigbee:** integration="zha" or "zigbee2mqtt". Returns ieee_address, radio metrics. **Z-Wave:** integration="zwave_js". Returns node_id, node_status. **Matter:** integration="matter". Returns node_diagnostics (network type, reachability, IPs, fabrics). For management use ha_manage_radio.
Parameters
device_id-(Annotated[str | None, Field(description='Device ID to retrieve details for. If omitted, lists devices.', default=None)])= nullentity_id-(Annotated[str | None, Field(description="Entity ID to find the associated device for (e.g., 'light.living_room')", default=None)])= nullintegration-(Annotated[str | None, Field(description="Filter devices by integration: 'zha', 'zigbee2mqtt', 'zwave_js', 'mqtt', 'hue', etc.", default=None)])= nullarea_id-(Annotated[str | None, Field(description="Filter devices by area ID (e.g., 'living_room')", default=None)])= nullmanufacturer-(Annotated[str | None, Field(description="Filter devices by manufacturer name (e.g., 'Philips')", default=None)])= nulllimit-(Annotated[int, Field(default=50, ge=1, le=200, description='Max devices to return per page in list mode (default: 50)')])= 50offset-(Annotated[int, Field(default=0, ge=0, description='Number of devices to skip for pagination (default: 0)')])= 0detail_level-(Annotated[Literal['summary', 'full'], Field(default='summary', description="'summary': basic device info and protocol identifiers (default for list mode). 'full': include entities and all integration details. Single device lookups always return full detail.")])= "summary"Description
Remove an orphaned device from the Home Assistant device registry. WARNING: This removes the device entry from the registry. - Use only for orphaned devices that are no longer connected - Active devices will typically be re-added by their integration - Associated entities may also be removed This uses the config entry removal which is the safe way to remove devices. If the device has multiple config entries, they must all be removed. EXAMPLES: - Remove orphaned device: ha_remove_device("abc123def456") NOTE: For most use cases, consider disabling the device instead: ha_set_device(device_id="abc123", disabled_by="user")
Parameters
device_idrequired-(Annotated[str, Field(description='Device ID to remove from the registry')])Description
Update device properties such as name, area, disabled state, or labels. IMPORTANT: Renaming a device does NOT rename its entities! Device and entity names are independent. To rename entities, use ha_set_entity(new_entity_id=...). Common workflow for full rename: 1. ha_set_device(device_id="abc", name="Living Room Sensor") # Rename device 2. ha_set_entity("sensor.old", new_entity_id="sensor.living_room") # Rename entities separately PARAMETERS: - name: Sets the user-defined display name (name_by_user) - area_id: Assigns device to an area/room. Use '' to remove from area. - disabled_by: Set to 'user' to disable, or empty to enable - labels: List of labels (replaces existing labels) EXAMPLES: - Rename device: ha_set_device("abc123", name="Living Room Hub") - Move to area: ha_set_device("abc123", area_id="living_room") - Disable device: ha_set_device("abc123", disabled_by="user") - Enable device: ha_set_device("abc123", disabled_by="") - Add labels: ha_set_device("abc123", labels=["important", "sensor"])
Parameters
device_idrequired-(Annotated[str, Field(description='Device ID to update')])name-(Annotated[str | None, Field(description='New display name for the device (sets name_by_user)', default=None)])= nullarea_id-(Annotated[str | None, Field(description="Area/room ID to assign the device to. Use empty string '' to unassign.", default=None)])= nulldisabled_by-(Annotated[str | None, Field(description="Set to 'user' to disable, or None/empty string to enable", default=None)])= nulllabels-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(description='Labels to assign to the device (replaces existing labels)', default=None)])= nullEnergy
Description
Manage the Home Assistant Energy Dashboard preferences. The Energy Dashboard configuration (grid/solar/battery/gas/water energy sources, individual device consumption sensors for electricity and water, cost tariffs) is stored in ``.storage/energy`` and not otherwise reachable via REST, services, or helper flows — this tool is the only way for agents to inspect or modify it. WHEN TO USE: - mode='get' / 'set': inspect or replace the full Energy Dashboard config. Use 'set' for bulk edits or anything touching multiple top-level keys at once. - mode='add_device' / 'remove_device': add or remove a single device-consumption entry. The tool performs a fresh read-modify-write internally; the caller does NOT manage config_hash. Use ``water=True`` to target the water meter list instead of electricity. - mode='add_source': append a single entry to ``energy_sources`` (grid, solar, battery, gas, or water). Same atomic read-modify-write semantics. WHEN NOT TO USE: - To create the underlying statistics themselves — they must already exist as HA entities before being referenced here; create them via the relevant integration's config flow first. CAVEATS: - ``energy/save_prefs`` has per-key FULL-REPLACE semantics. Passing ``{"device_consumption": [<one entry>]}`` deletes every other device the user had configured — silently, with no error. mode='set' requires a fresh ``config_hash`` for optimistic locking; convenience modes hide this entirely. - ``config_hash`` accepts both a single ``str`` (full-blob lock) and a ``dict[_PrefsKey, str]`` keyed by top-level keys (per-key lock, taken from the ``config_hash_per_key`` field of the mode='get' response). The per-key form lets an agent submit only the top- level key it wants to change — set-equality between ``config`` keys and dict keys is enforced, and any key outside the canonical set (typo, etc.) on either side is rejected with ``VALIDATION_FAILED`` rather than silently dropped (so an empty submission cannot succeed as a no-op). A per-key submission still fully replaces that key's value as the save endpoint requires. Mismatch on any locked key returns ``RESOURCE_LOCKED`` with the offending keys in the response's top-level ``mismatched_keys`` (``create_error_response`` flattens the ``context`` dict onto the response root). - ``dry_run=True`` skips the hash check entirely for both forms; the per-key form is therefore silently accepted on dry runs even if its keys would mismatch the current state. - A local shape check runs before every write; malformed payloads are rejected with a ``shape_errors`` list. - After a successful write, the tool calls ``energy/validate`` and returns any residual issues as ``post_save_validation_errors`` in the response. These reflect semantic problems (missing stats, unit mismatches) that shape checks can't catch; the save persists regardless — correct the config and write again if needed. - The underlying save endpoint is admin-only. Non-admin tokens will receive an authorization error from Home Assistant. - Convenience modes are NOT idempotent: 'add_device' on an existing ``stat_consumption`` returns RESOURCE_ALREADY_EXISTS; 'remove_device' on a missing entry returns RESOURCE_NOT_FOUND. 'add_source' rejects duplicates by ``(type, stat_energy_from)`` for solar/battery/gas/water (RESOURCE_ALREADY_EXISTS); grid entries are appended without a duplicate check (multiple grid variants are legitimate, and grid has no single canonical uniqueness key) — the caller is responsible for de-duplicating grid sources. - Convenience modes do NOT bypass the local shape check on dry_run: ``dry_run=True`` still raises ``RESOURCE_ALREADY_EXISTS`` (duplicate add_device / add_source), ``RESOURCE_NOT_FOUND`` (missing remove_device), or ``VALIDATION_FAILED`` (post-mutator shape error) when the proposed mutation is not applicable. The mutator and shape check both run before the dry-run short-circuit.
Parameters
moderequired-(Annotated[Literal['get', 'set', 'add_device', 'remove_device', 'add_source'], Field(description="Operation mode. Primitives: 'get' reads the current prefs; 'set' writes a full prefs payload (per-top-level-key full-replace). Convenience modes: 'add_device' / 'remove_device' / 'add_source' perform a single read-modify-write atomically — no config_hash from the caller, the tool fetches it fresh internally.")])config-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION, Field(description="Full prefs payload for mode='set'. Must contain the top-level keys you intend to replace: 'energy_sources', 'device_consumption', 'device_consumption_water'. Any top-level key present in this payload REPLACES the existing list entirely; any omitted key is preserved. Call with mode='get' first, mutate the returned config, then pass the whole object back. Ignored by convenience modes.", default=None)])= nullconfig_hash-(Annotated[str | dict[_PrefsKey, str] | None, Field(description="Hash from a previous mode='get' call. REQUIRED for mode='set' unless dry_run=True. Two forms: str (full-blob lock) or dict (per-key lock, taken from the config_hash_per_key field of mode='get'). Pass the dict form as a native object, NOT a JSON-encoded string — a stringified dict is treated as a full-blob token and will report RESOURCE_LOCKED; clients that can only send strings should use the str full-blob form. See the tool docstring for fail-closed semantics. Ignored by convenience modes.", default=None)])= nulldry_run-(Annotated[bool, Field(description="If True, no write is performed. For mode='set': runs a local shape check on the proposed config AND calls the server's energy/validate against the CURRENT persisted state (Home Assistant's validate endpoint cannot validate an unsubmitted payload). For convenience modes: simulates the mutation against a fresh read and reports what would change without writing — but still raises RESOURCE_ALREADY_EXISTS (duplicate add_device, or duplicate add_source for solar/battery/gas/water), RESOURCE_NOT_FOUND (missing remove_device), or VALIDATION_FAILED (post-mutator shape error) when the proposed mutation is not applicable. Default False.", default=False)])= falsestat_consumption-(Annotated[str | None, Field(description="Statistic entity_id for mode='add_device' / 'remove_device' (e.g. 'sensor.fridge_energy'). Required for those modes; ignored otherwise.", default=None)])= nullname-(Annotated[str | None, Field(description="Optional display name for mode='add_device'. Only used when adding a new device entry; ignored otherwise.", default=None)])= nullincluded_in_stat-(Annotated[str | None, Field(description="Optional 'parent' statistic for mode='add_device'. Set this to a statistic that already INCLUDES this device's consumption (e.g., a whole-home or circuit-level meter that this device feeds into). The Energy Dashboard will subtract this device's reading from the parent so the parent's contribution is not double-counted. Ignored otherwise.", default=None)])= nullwater-(Annotated[bool, Field(description="If True, mode='add_device' / 'remove_device' targets 'device_consumption_water' instead of 'device_consumption'. Default False.", default=False)])= falsesource-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION, Field(description="Single energy_sources entry for mode='add_source'. Must contain 'type' (one of grid|solar|battery|gas|water) and the type-specific required fields (e.g. solar/battery/gas/water require 'stat_energy_from'). Every source type also accepts an optional 'name' (display label in the energy graphs); battery additionally accepts 'stat_soc' (state-of-charge statistic). Note: HA Core's voluptuous schema for grid sources requires the full field set (cost_adjustment_day, stat_energy_to, stat_cost, entity_energy_price, number_energy_price, entity_energy_price_export, number_energy_price_export, stat_compensation) — the local shape check is narrower, so a minimal {'type': 'grid'} passes locally but surfaces in post_save_validation_errors after writing. Pass the unused fields as None to satisfy the server. Required for mode='add_source'; ignored otherwise.", default=None)])= nullEntity Registry
Description
Get entity registry information for one or more entities. Returns detailed entity registry metadata including area assignment, custom name/icon, enabled/hidden state, aliases, labels, and more. RESOLVER MODE: Pass unique_id (instead of entity_id) to resolve a stable integration unique_id to its entity_id(s). Since the registry's unique key is (domain, platform, unique_id), the same unique_id can match multiple platforms — all matches are returned in entity_entries with a `matches` count. Narrow with domain/platform. Resolver reads as_partial_dict, so aliases and the device_class override come back as defaults ([]/null). RELATED TOOLS: - ha_set_entity(): Modify entity properties (area, name, icon, enabled, hidden, aliases) - ha_get_state(): Get current state/attributes (on/off, temperature, etc.) - ha_search(): Find entities by name, domain, or area EXAMPLES: - Single entity: ha_get_entity("sensor.temperature") - Multiple entities: ha_get_entity(["light.living_room", "switch.porch"]) RESPONSE FIELDS: - entity_id: Full entity identifier - name: Custom display name (null if using original_name) - original_name: Default name from integration - icon: Custom icon (null if using default) - area_id: Assigned area/room ID (null if unassigned) - disabled_by: Why disabled (null=enabled, "user"/"integration"/etc) - hidden_by: Why hidden (null=visible, "user"/"integration"/etc) - enabled: Boolean shorthand (True if disabled_by is null) - hidden: Boolean shorthand (True if hidden_by is not null) - aliases: Voice assistant aliases - labels: Assigned label IDs - categories: Category assignments (dict mapping scope to category_id) - device_class: User "Show As" override (null = use original_device_class) - original_device_class: Default device class from the integration - options: Per-domain registry options (e.g. sensor display_precision). Voice-assistant exposure is also stored here but should be set/cleared via the ha_set_entity(expose_to=...) parameter, not the options dict. - platform: Integration platform (e.g., "hue", "zwave_js") - device_id: Associated device ID (null if standalone) - config_entry_id: Parent config entry's ID (null for YAML-only entities). When non-null — e.g. for UI-created template/group/ utility_meter/derivative/... helpers — pass it to ``ha_get_integration(entry_id=..., include_options=True)`` to read the helper's current config (template body, group members, etc.) without scanning a domain list. - unique_id: Integration's unique identifier Resolved-name enrichment (present only when the ha_mcp_tools component advertises it; otherwise these keys are absent): - area: Assigned area NAME (device-inherited when the entity has none; resolves area_id above) - floor: Floor NAME of the assigned area - label_names: Assigned label NAMES (resolves the label ids in labels) Resolved label names live under label_names HERE (this tool's base `labels` already carries the label ids); ha_search result_fields and ha_get_entity_exposure instead emit the resolved names under `labels`.
Parameters
entity_id-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(description="Entity ID or list of entity IDs to retrieve (e.g., 'sensor.temperature' or ['light.living_room', 'switch.porch']). Mutually exclusive with unique_id.", default=None)])= nullunique_id-(Annotated[str | None, Field(description='Resolve a stable integration unique_id to its entity_id(s) (entity_id is mutable, unique_id is not). Mutually exclusive with entity_id. Optionally narrow with domain/platform.', default=None)])= nulldomain-(Annotated[str | None, Field(description="Resolver filter (unique_id mode only): restrict matches to this entity domain, e.g. 'sensor'.", default=None)])= nullplatform-(Annotated[str | None, Field(description="Resolver filter (unique_id mode only): restrict matches to this integration platform, e.g. 'hue'.", default=None)])= nullDescription
Get entity exposure settings - list all or get settings for a specific entity. Without an entity_id: Lists all entities and their exposure status to voice assistants (Alexa, Google Assistant, Assist). With an entity_id: Returns which voice assistants the specific entity is exposed to. EXAMPLES: - List all exposures: ha_get_entity_exposure() - Filter by assistant: ha_get_entity_exposure(assistant="cloud.alexa") - Get specific entity: ha_get_entity_exposure(entity_id="light.living_room") RETURNS (when listing): - exposed_entities: Dict mapping entity_ids to their exposure status - summary: Count of entities exposed to each assistant RETURNS (when getting specific entity): - exposed_to: Dict of assistant -> True/False for each assistant - is_exposed_anywhere: True if exposed to at least one assistant When the ha_mcp_tools component advertises the exposure capability, each record is additively enriched with the entity's name/area so no second ha_search is needed to identify it: friendly_name, domain, area, floor, and labels (plus state for entities that have one) on a single-entity lookup, and a parallel entity_info map keyed by entity_id when listing. These fields are absent when the component is unavailable.
Parameters
entity_id-(Annotated[str | None, Field(description='Entity ID to check exposure settings for. If omitted, lists all entities with exposure settings.', default=None)])= nullassistant-(Annotated[str | None, Field(description="Filter by assistant: 'conversation', 'cloud.alexa', or 'cloud.google_assistant'. If not specified, returns all.", default=None)])= nullDescription
Remove one or more entities from the Home Assistant entity registry. Permanently removes the entity registration from Home Assistant. The entity will no longer appear in the UI or be available to automations. WARNING: This permanently removes the entity registration. - Use only for orphaned or stale entity entries - If the underlying device or integration is still active, the entity may be re-added automatically on the next HA restart or reload - This action cannot be undone without restoring from backup BULK MODE: Pass a list of entity IDs to remove up to 100 at once — handy for clearing the restored=true orphans an integration leaves behind after its filters change. Removals run sequentially and return: {removed: [...], skipped: [...], errors: [{entity_id, code, message}]} where skipped = ids already absent (not-found is idempotent, not an error). Bulk mode is NOT auto-backed-up (the snapshot is single-entity); single-id removal still is. EXAMPLES: - Remove orphaned sensor: ha_remove_entity("sensor.old_temperature") - Remove stale helper entry: ha_remove_entity("input_boolean.deleted_helper") - Bulk cleanup: ha_remove_entity(["sensor.orphan_1", "sensor.orphan_2"]) NOTE: For most use cases, consider disabling instead: ha_set_entity(entity_id="sensor.old", enabled=False) RELATED TOOLS: - ha_search: Find entities to verify the entity_id before removing - ha_get_entity: Check entity details before removal
Parameters
entity_idrequired-(Annotated[str | list[str], JSON_STRING_COERCION, Field(description="Entity ID, or a list of entity IDs, to remove from the entity registry (e.g., 'sensor.old_temperature'). Permanently removes the registration(s).")])Description
Update entity properties in the entity registry. Allows modifying entity metadata such as area assignment, display name, icon, "Show As" device class override, per-domain registry options, enabled/disabled state, visibility, aliases, labels, voice assistant exposure, and entity_id rename in a single call. BULK OPERATIONS: When entity_id is a list, only labels, expose_to, and categories parameters are supported. Other parameters (area_id, name, icon, device_class, options, enabled, hidden, aliases, new_entity_id, new_device_name) require single entity. LABEL OPERATIONS: - label_operation="set" (default): Replace all labels with the provided list. Use [] to clear. - label_operation="add": Add labels to existing ones without removing any. - label_operation="remove": Remove specified labels from the entity. SHOW AS / DEVICE CLASS: device_class overrides the entity's display device class — equivalent to the HA UI's "Show As" dropdown. Use empty string '' to clear. Applies instantly, no reload needed. REGISTRY OPTIONS: options carries per-domain registry options (sensor display_precision, weather forecast_type, etc). Pass {domain: {key: value}}; multi-domain dicts are sent as separate registry updates because HA's WS schema requires options_domain + options to be paired one domain at a time. ENTITY ID RENAME: Use new_entity_id to change an entity's ID (e.g., sensor.old -> sensor.new). Domain must match. Voice exposure settings are preserved automatically. WARNING: Renaming an entity_id does NOT update references in automations, scripts, templates, or dashboards. All consumers of the old entity_id must be updated manually — HA does not propagate the rename automatically. Rename limitations: - Entity history is preserved (HA 2022.4+) - Entities without unique IDs cannot be renamed - Entities disabled by their integration cannot be renamed DEVICE RENAME: Use new_device_name to rename the associated device. Can be combined with new_entity_id to rename both in one call. The device is looked up automatically. Use ha_search() or ha_get_device() to find entity IDs. Use ha_config_get_label() to find available label IDs. EXAMPLES: Single entity: - Assign to area: ha_set_entity("sensor.temp", area_id="living_room") - Rename display name: ha_set_entity("sensor.temp", name="Living Room Temperature") - Set Show As: ha_set_entity("binary_sensor.zone_10", device_class="window") - Clear Show As: ha_set_entity("binary_sensor.zone_10", device_class="") - Set sensor precision: ha_set_entity("sensor.power", options={"sensor": {"display_precision": 2}}) - Rename entity_id: ha_set_entity("light.old_name", new_entity_id="light.new_name") - Rename entity and device: ha_set_entity("light.old", new_entity_id="light.new", new_device_name="New Lamp") - Rename entity_id with friendly name: ha_set_entity("sensor.old", new_entity_id="sensor.new", name="New Name") - Set labels: ha_set_entity("light.lamp", labels=["outdoor", "smart"]) - Add labels: ha_set_entity("light.lamp", labels=["new_label"], label_operation="add") - Remove labels: ha_set_entity("light.lamp", labels=["old_label"], label_operation="remove") - Clear labels: ha_set_entity("light.lamp", labels=[]) - Expose to Alexa: ha_set_entity("light.lamp", expose_to={"cloud.alexa": True}) Bulk operations: - Set labels on multiple: ha_set_entity(["light.a", "light.b"], labels=["outdoor"]) - Add labels to multiple: ha_set_entity(["light.a", "light.b"], labels=["new"], label_operation="add") - Expose multiple to Alexa: ha_set_entity(["light.a", "light.b"], expose_to={"cloud.alexa": True}) ENABLED/DISABLED WARNING: Setting enabled=False performs a **registry-level disable** — the entity is completely removed from the Home Assistant state machine and hidden from the UI. It will NOT appear in state queries, dashboards, or automations until re-enabled AND the integration is reloaded. This is NOT the same as "turning off" an entity. For automations and scripts, enabled=False is blocked. Use these instead: - ha_call_service("automation", "turn_off", entity_id="automation.xxx") - ha_call_service("script", "turn_off", entity_id="script.xxx")
Parameters
entity_idrequired-(Annotated[str | list[str], JSON_STRING_COERCION, Field(description='Entity ID or list of entity IDs to update. Bulk operations (list) only support labels, expose_to, and categories parameters.')])area_id-(Annotated[str | None, Field(description="Area/room ID to assign the entity to. Use empty string '' to unassign from current area. Single entity only.", default=None)])= nullname-(Annotated[str | None, Field(description="Display name for the entity. Use empty string '' to remove custom name and revert to default. Single entity only.", default=None)])= nullicon-(Annotated[str | None, Field(description="Icon for the entity (e.g., 'mdi:thermometer'). Use empty string '' to remove custom icon. Single entity only.", default=None)])= nulldevice_class-(Annotated[str | None, Field(description="Override the entity's display device class — what the HA UI's 'Show As' dropdown writes. Use empty string '' to clear the override and fall back to the integration default. None (the default) means 'no change' — pass an explicit '' to clear. Single entity only. Examples: 'window', 'door', 'motion' for binary_sensor; 'temperature', 'humidity' for sensor.", default=None)])= nulloptions-(Annotated[dict[str, dict[str, Any]] | None, JSON_STRING_COERCION, Field(description='Per-domain entity registry options (e.g. sensor \'display_precision\', weather \'forecast_type\'). Pass a dict mapping domain to a sub-dict, e.g. {"sensor": {"display_precision": 2}}. Multiple domains are sent as separate registry updates. For \'Show As\' use the dedicated `device_class` parameter — that is what the HA UI Show As dropdown writes. Voice-assistant exposure is stored under `options.<assistant>.should_expose` but must be managed via the dedicated `expose_to` parameter, not this options dict. Single entity only.', default=None)])= nullenabled-(Annotated[bool | None, Field(description='True to enable the entity, False to disable it. Single entity only. WARNING: Setting enabled=False is a registry-level disable — it completely removes the entity from the state machine and hides it from the UI. A reload or restart is required to restore it after re-enabling. NOT allowed for automation or script entities — use automation.turn_off / script.turn_off via ha_call_service() instead.', default=None)])= nullhidden-(Annotated[bool | None, Field(description='True to hide the entity from UI, False to show it. Single entity only.', default=None)])= nullaliases-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(description='List of voice assistant aliases for the entity (replaces existing aliases). Single entity only.', default=None)])= nullcategories-(Annotated[dict[str, str | None] | None, JSON_STRING_COERCION, Field(description='Category assignment as a dict mapping scope to category_id. Example: {"automation": "category_id_here"}. Use null value to clear: {"automation": null}. Single entity only.', default=None)])= nulllabels-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(description='List of label IDs for the entity. Behavior depends on label_operation parameter. Supports bulk operations.', default=None)])= nulllabel_operation-(Annotated[Literal['set', 'add', 'remove'], Field(description="How to apply labels: 'set' replaces all labels, 'add' adds to existing, 'remove' removes specified labels.", default='set')])= "set"expose_to-(Annotated[dict[str, bool] | None, JSON_STRING_COERCION, Field(description='Control voice assistant exposure. Pass a dict mapping assistant IDs to booleans. Valid assistants: \'conversation\' (Assist), \'cloud.alexa\', \'cloud.google_assistant\'. Example: {"conversation": true, "cloud.alexa": false}. Supports bulk operations.', default=None)])= nullnew_entity_id-(Annotated[str | None, Field(description="New entity ID to rename to (e.g., 'light.new_name'). Domain must match the original. Single entity only.", default=None)])= nullnew_device_name-(Annotated[str | None, Field(description='New display name for the associated device. If provided, both entity and device are updated in one operation. Single entity only.', default=None)])= nullFiles
Description
Delete a file from allowed directories in the Home Assistant config. Permanently removes a file from the allowed directories. This action cannot be undone. **Allowed Delete Directories:** - `www/` - Web assets - `themes/` - Theme files - `custom_templates/` - Template files - `dashboards/` - YAML-mode dashboard files - Plus any custom directories OR HAOS sibling volumes (`/share`, `/media`, `/ssl`, `/backup`) configured in the ha-mcp settings UI (pass the absolute path for volumes) **Security:** - Only the directories above allow deletions - Configuration files cannot be deleted - Path traversal (../) is blocked - Requires confirm=True to prevent accidents **Returns:** - success: Whether the operation succeeded - path: The file path that was deleted - message: Confirmation message **Example:** ```python # Delete an old CSS file result = ha_delete_file( path="www/deprecated-style.css", confirm=True ) ```
Parameters
pathrequired-(Annotated[str, Field(description="File path. Must be in a writable built-in dir (www/, themes/, custom_templates/, dashboards/), a configured custom directory, or a configured HAOS sibling volume (/share, /media, /ssl, /backup — pass the absolute path). Example: 'www/old-file.css'")])confirm-(Annotated[bool, Field(default=False, description='Must be True to confirm deletion. This is a safety measure to prevent accidental deletions.')])= falseDescription
List files in a directory within the Home Assistant config directory. Lists files in allowed directories (www/, themes/, custom_templates/, dashboards/, blueprints/) with optional glob pattern filtering. Returns file names, sizes, and modification times. **Allowed Directories:** - `www/` - Web assets (CSS, JS, images for dashboards) - `themes/` - Theme files - `custom_templates/` - Jinja2 template files - `dashboards/` - YAML-mode dashboard files - `blueprints/` - Automation/script blueprint sources (read-only) - Your configured `packages/` folder, when `homeassistant: packages:` is set (the folder name you bound, default `packages/`) - Plus any custom directories OR HAOS sibling volumes (`/share`, `/media`, `/ssl`, `/backup`) configured in the ha-mcp settings UI (pass the absolute path for volumes) **Security:** Only directories in the allowed list can be accessed. Path traversal attempts (../) are blocked. **Returns:** - success: Whether the operation succeeded - path: The directory path that was listed - files: List of file info objects with name, size, is_dir, modified - count: Number of files found **Example:** ```python # List all CSS files in www/ result = ha_list_files(path="www/", pattern="*.css") ```
Parameters
pathrequired-(Annotated[str, Field(description="Directory path. Relative to the config dir for the built-in allowlist (www/, themes/, custom_templates/, dashboards/, blueprints/). Custom directories and HAOS sibling volumes (/share, /media, /ssl, /backup) configured in the ha-mcp settings UI are also allowed (pass the absolute path). Example: 'www/' or '/share/llm'")])pattern-(Annotated[str | None, Field(default=None, description="Optional glob pattern to filter files. Example: '*.css', '*.yaml', '*.js'")])= nullDescription
Read a file from the Home Assistant config directory. General-purpose escape hatch — prefer a dedicated tool when one exists: ha_get_blueprint for a blueprint body, ha_config_get_yaml for a config key, ha_config_get_automation/script/scene for storage-mode items. Reach for ha_read_file only for raw on-disk text those tools don't expose. Reads files from allowed paths within the config directory. Some files have special handling: - `secrets.yaml`: Values are masked for security - `home-assistant.log`: Limited to tail (last N lines) by default **Allowed Read Paths:** - `configuration.yaml`, `automations.yaml`, `scripts.yaml`, `scenes.yaml` - `secrets.yaml` (values masked) - `packages/*.yaml` - `home-assistant.log` (tail only) - `www/**`, `themes/**`, `custom_templates/**`, `dashboards/**`, `blueprints/**` - `custom_components/**/*.py` (read-only) - Plus any custom directories OR HAOS sibling volumes (`/share`, `/media`, `/ssl`, `/backup`) configured in the ha-mcp settings UI (pass the absolute path for volumes) **Security:** - Path traversal (../) is blocked - Only allowed paths can be read - Sensitive data in secrets.yaml is masked **Returns:** - success: Whether the operation succeeded - content: The file content (may be truncated for logs) - size: File size in bytes - modified: Last modification timestamp - path: The file path that was read - subtree: Round-trip text of the `yaml_path` key, when that arg is set (null when the key is absent). Comments and HA tags (`!secret`, `!include`) survive as written — a `!secret` is never resolved. **Example:** ```python # Read configuration result = ha_read_file(path="configuration.yaml") # Read last 100 lines of log result = ha_read_file(path="home-assistant.log", tail_lines=100) # Read just the alert2 block out of a package file result = ha_read_file(path="packages/alert2.yaml", yaml_path="alert2") ```
Parameters
pathrequired-(Annotated[str, Field(description="File path. Relative to the config dir for the built-in allowlist; absolute for a configured HAOS sibling volume (/share, /media, /ssl, /backup). Examples: 'configuration.yaml', 'www/custom.css', '/share/llm/notes.md'")])tail_lines-(Annotated[int | None, Field(default=None, ge=1, le=10000, description='For log files, return only the last N lines. Recommended for home-assistant.log to avoid large responses. Default: None (return full file, or last 1000 lines for logs)')])= nullyaml_path-(Annotated[str | None, Field(default=None, description="Dotted YAML key path (e.g. 'alert2', 'mqtt.sensor'). When set, the response also carries 'subtree': the round-trip text of just that key's value. To look a key up across packages/*.yaml, or to get it as structured data, use ha_config_get_yaml instead.")])= nullDescription
Write a file to allowed directories in the Home Assistant config. Creates or updates files in restricted directories only. This is useful for: - Creating custom CSS/JS for dashboards - Creating Jinja2 templates **Allowed Write Directories:** - `www/` - Web assets for dashboards - `themes/` - Theme YAML files - `custom_templates/` - Jinja2 template files - `dashboards/` - YAML-mode dashboard files - Plus any custom directories OR HAOS sibling volumes (`/share`, `/media`, `/ssl`, `/backup`) configured in the ha-mcp settings UI (pass the absolute path for volumes) **Security:** - Only the directories above allow writes - Configuration files (configuration.yaml, etc.) cannot be written - Path traversal (../) is blocked Text content only. Overwriting a file that currently holds binary content still succeeds, but its prior bytes cannot be captured by auto-backup (only modifications/deletions of text files are snapshotted); the skip is logged, the write is not blocked. **Returns:** - success: Whether the operation succeeded - path: The file path that was written - size: Size of the written file in bytes - created: Whether this was a new file (vs overwrite) **Example:** ```python # Create a custom CSS file result = ha_write_file( path="www/custom-dashboard.css", content=".card { background: #333; }", overwrite=True ) # Create a custom Jinja template file result = ha_write_file( path="custom_templates/formatters.jinja", content="{% macro shout(text) %}{{ text | upper }}{% endmacro %}", overwrite=False ) ```
Parameters
pathrequired-(Annotated[str, Field(description="File path. Must be in a writable built-in dir (www/, themes/, custom_templates/, dashboards/), a configured custom directory, or a configured HAOS sibling volume (/share, /media, /ssl, /backup — pass the absolute path). Example: 'www/custom.css', '/share/llm/out.txt'")])contentrequired-(Annotated[str, Field(description='The content to write to the file.')])overwrite-(Annotated[bool, Field(default=False, description='Whether to overwrite if file exists. Default is False to prevent accidental overwrites.')])= falsecreate_dirs-(Annotated[bool, Field(default=True, description="Whether to create parent directories if they don't exist. Default is True.")])= trueGroups
Description
List Home Assistant entity groups with their member entities. Returns one page of groups created via group.set service or YAML configuration; `total_count` and `has_more` report the full set. Each group includes: - Entity ID (group.xxx) - Friendly name - State (on/off based on member states) - Member entities - Icon (if set) - All mode (if all entities must be on) EXAMPLES: - First page of groups: ha_config_list_groups() - Next page: ha_config_list_groups(offset=100) **NOTE:** This returns old-style groups (created via group.set or YAML). Platform-specific groups (light groups, cover groups) are separate entities.
Parameters
limit-(Annotated[int, Field(default=100, ge=1, le=500, description='Max groups to return per page (default: 100)')])= 100offset-(Annotated[int, Field(default=0, ge=0, description='Number of groups to skip for pagination (default: 0)')])= 0Description
Remove a service-based Home Assistant entity group via the group.remove service. **When NOT to use:** for groups created through `ha_config_set_helper(helper_type="group", ...)`, use `ha_remove_helpers_integrations`. Those config-entry-backed groups are not reachable via the group.remove service. **When to use:** removing groups created with `ha_config_set_group` or defined in YAML via `group:` configuration. Config-entry-backed deletion tools cannot find these. EXAMPLES: - Remove group: ha_config_remove_group("living_room_lights") Use ha_config_list_groups() to find existing groups. **WARNING:** - Removing a group used in automations may cause those automations to fail. - Groups defined in YAML can be removed at runtime but will reappear after restart. - This only removes old-style groups, not platform-specific groups.
Parameters
object_idrequired-(Annotated[str, Field(description="Group identifier without 'group.' prefix (e.g., 'living_room_lights')")])wait-(Annotated[bool, Field(description='Wait for group to be fully removed before returning. Default: True.', default=True)])= trueDescription
Create or update a service-based Home Assistant entity group via the group.set service. **When NOT to use:** for typical "combine these entities into one controllable group" requests, prefer `ha_config_set_helper(helper_type="group", ...)`. Config-entry-backed groups are registered in the entity registry, so `ha_set_entity` can assign them to areas and they are deletable via `ha_remove_helpers_integrations`. **When to use:** compatibility with existing groups already configured via group.set or YAML, or the rare case where entity-registry membership is explicitly unwanted. Groups created here are only removable via `ha_config_remove_group` — `ha_remove_helpers_integrations` will not find them. **For NEW groups:** Provide object_id and entities (required). **For EXISTING groups:** Provide object_id and any fields to update. EXAMPLES: - Create group: ha_config_set_group("bedroom_lights", entities=["light.lamp", "light.ceiling"]) - Create with name: ha_config_set_group("sensors", entities=["sensor.temp"], name="All Sensors") - Update name: ha_config_set_group("lights", name="Living Room Lights") - Add entities: ha_config_set_group("lights", add_entities=["light.extra"]) - Remove entities: ha_config_set_group("lights", remove_entities=["light.old"]) - Replace all entities: ha_config_set_group("lights", entities=["light.new1", "light.new2"]) **NOTE:** entities, add_entities, and remove_entities are mutually exclusive.
Parameters
object_idrequired-(Annotated[str, Field(description="Group identifier without 'group.' prefix (e.g., 'living_room_lights')")])entities-(Annotated[list[str] | None, JSON_STRING_COERCION, Field(description='List of entity IDs for the group. Required when creating new group. When updating, replaces all entities (mutually exclusive with add_entities/remove_entities).', default=None)])= nullname-(Annotated[str | None, Field(description='Friendly display name for the group', default=None)])= nullicon-(Annotated[str | None, Field(description="Material Design Icon (e.g., 'mdi:lightbulb-group')", default=None)])= nullall_on-(Annotated[bool | None, Field(description='If True, all entities must be on for group to be on (default: False)', default=None)])= nulladd_entities-(Annotated[list[str] | None, JSON_STRING_COERCION, Field(description='Add these entities to an existing group (mutually exclusive with entities)', default=None)])= nullremove_entities-(Annotated[list[str] | None, JSON_STRING_COERCION, Field(description='Remove these entities from an existing group (mutually exclusive with entities)', default=None)])= nullwait-(Annotated[bool, Field(description='Wait for group to be queryable before returning. Default: True. Set to False for bulk operations.', default=True)])= trueHACS
Description
Get HACS (Home Assistant Community Store) data — search the store or fetch repository details. Use ``action="search"`` to search/browse/list store repositories, or ``action="info"`` for one repository's full details (README, versions, GitHub stats). This tool is read-only; to install or add repositories use ``ha_manage_hacs``, and for non-HACS entities/config use the domain-specific tools. **DASHBOARD TIP:** ``action="search", installed_only=True, category="lovelace"`` discovers installed custom cards to wire into ``ha_config_set_dashboard()``. **Examples:** - Search the store: ha_get_hacs_info(action="search", query="mushroom", category="lovelace") - List installed: ha_get_hacs_info(action="search", installed_only=True) - Repository details: ha_get_hacs_info(action="info", repository_id="441028036") **Caveats:** ``info`` fetches full repository detail from GitHub, so it can hit GitHub rate limits / needs HACS's configured GitHub token; ``search`` reads HACS's locally cached repository index. ``repository_id`` accepts a numeric HACS ID or an ``owner/repo`` path.
Parameters
actionrequired-(Annotated[Literal['search', 'info'], Field(description="'search' the store, or 'info' for one repository")])query-(Annotated[str, Field(description="Search keyword (action='search')")])= ""category-(Annotated[Literal['integration', 'lovelace', 'theme', 'appdaemon', 'python_script'] | None, Field(description="Filter by category (action='search')")])= nullinstalled_only-(Annotated[bool, Field(description="Only return installed repositories (action='search', default: False)")])= falsemax_results-(Annotated[int, Field(ge=1, le=100, description="Maximum number of results (action='search', default: 10, max: 100)")])= 10offset-(Annotated[int, Field(ge=0, description="Results to skip for pagination (action='search', default: 0)")])= 0repository_id-(Annotated[str | None, Field(description="Numeric HACS ID or 'owner/repo' path (action='info')")])= nullDescription
Manage HACS (Home Assistant Community Store) — install/update or add custom repositories. Use ``action="download"`` to install or update a repository, or ``action="add_repository"`` to register a custom GitHub repository with HACS. This tool performs writes; to search the store or read repository details use ``ha_get_hacs_info``. **Examples:** - Install latest: ha_manage_hacs(action="download", repository_id="441028036") - Install a version: ha_manage_hacs(action="download", repository_id="piitaya/lovelace-mushroom", version="v4.0.0") - Add a custom repo: ha_manage_hacs(action="add_repository", repository="owner/repo", category="lovelace") **Caveats:** Installing an integration usually needs a Home Assistant restart to activate; new Lovelace cards need a browser cache clear. ``repository_id`` accepts a numeric HACS ID or an ``owner/repo`` path; ``add_repository`` requires ``owner/repo`` format plus a matching ``category``.
Parameters
actionrequired-(Annotated[Literal['download', 'add_repository'], Field(description="'download' to install/update, or 'add_repository'")])repository_id-(Annotated[str | None, Field(description="Numeric HACS ID or 'owner/repo' path (action='download')")])= nullversion-(Annotated[str | None, Field(description="Specific version to install (action='download')")])= nullrepository-(Annotated[str | None, Field(description="GitHub repo 'owner/repo' to add (action='add_repository')")])= nullcategory-(Annotated[Literal['integration', 'lovelace', 'theme', 'appdaemon', 'python_script'] | None, Field(description="Repository category (action='add_repository')")])= nullHelper Entities
Description
List Home Assistant helpers of a specific type with their configurations. Returns one page of helpers; `total_count` and `has_more` report the full set. Each record carries the complete configuration for its helper, including: - id (immutable storage key), entity_id (current — address the helper by this, where available), name (current display name), original_name (creation-time name), icon - Type-specific settings (min/max for input_number, options for input_select, etc.) - Area and label assignments For a helper renamed in the UI, id/original_name keep the storage values while entity_id/name reflect the current entity registry (entity_id is the identifier ha_config_set_helper resolves against, so prefer it over id for a renamed helper). entity_id/original_name are present only for storage-collection helpers matched in the entity registry — types with no backing entity (e.g. tag), and every record when the registry read degrades, carry only id/name (a warning flags the degraded case). SUPPORTED HELPER TYPES: - input_button: Virtual buttons for triggering automations - input_boolean: Toggle switches/checkboxes - input_select: Dropdown selection lists - input_number: Numeric sliders/input boxes - input_text: Text input fields - input_datetime: Date/time pickers - counter: Counters with increment/decrement/reset - timer: Countdown timers with start/pause/cancel - schedule: Weekly schedules with time ranges (on/off per day) - zone: Geographical zones for presence detection - person: Person entities linked to device trackers - tag: NFC/QR tags for automation triggers EXAMPLES: - List all number helpers: ha_config_list_helpers("input_number") - List all counters: ha_config_list_helpers("counter") - List all zones: ha_config_list_helpers("zone") - List all persons: ha_config_list_helpers("person") - List all tags: ha_config_list_helpers("tag") - List every helper type at once: ha_config_list_helpers("all") - Next page: ha_config_list_helpers("input_boolean", offset=100) **NOTE:** Storage types list what HA's ``{type}/list`` command returns: the storage-backed helpers (created via UI/API), not the YAML-defined ones. ``person`` is the exception — HA lists its YAML-configured persons alongside the storage ones, so both appear here. Flow-based types (template / group / utility_meter / derivative / etc.) require the ha_mcp_tools custom component (>= 1.1.0) and are served only through it; storage types are listed on all installs. Requesting a flow type without the component returns a COMPONENT_NOT_INSTALLED error. Pass helper_type="all" to enumerate every helper type in a single call. Each record carries its own ``helper_type``. This mode is component-only (there is no single built-in command that lists all types): without the ha_mcp_tools component it returns a COMPONENT_NOT_INSTALLED error rather than a partial or empty list. For detailed helper documentation, use ha_get_skill_guide.
Parameters
helper_typerequired-(Annotated[Literal['input_button', 'input_boolean', 'input_select', 'input_number', 'input_text', 'input_datetime', 'counter', 'timer', 'schedule', 'zone', 'person', 'tag', 'all'] | SUPPORTED_HELPERS, Field(description="Helper type to list. Storage types are listed on all installs; flow-based types require the ha_mcp_tools custom component. Pass 'all' to list every helper type in one call (also requires the ha_mcp_tools component).")])limit-(Annotated[int, Field(default=100, ge=1, le=500, description='Max helpers to return per page (default: 100)')])= 100offset-(Annotated[int, Field(default=0, ge=0, description='Number of helpers to skip for pagination (default: 0)')])= 0Description
Create or update Home Assistant helper entities and config subentries (28 types, unified interface). MUST call ha_get_skill_guide OR refer to your locally installed skills first. SIMPLE/FLOW helper create requires `name`; SIMPLE/FLOW helper update requires `helper_id`. Config subentry create requires `entry_id` and `subentry_type`; config subentry update also requires `subentry_id`. SIMPLE types (structured params, WebSocket API): input_boolean, input_button, input_select, input_number, input_text, input_datetime, counter, timer, schedule, zone, person, tag. FLOW types (pass `config` dict, Config Entry Flow API): template, group, utility_meter, derivative, min_max, threshold, integration, statistics, trend, random, filter, tod, generic_thermostat, switch_as_x, generic_hygrostat. Note: `tod` is the purpose-built "is-current-time-in-range" indicator (supports cross-midnight ranges, unlike `schedule`). CONFIG_SUBENTRY type (Config Subentry Flow API): config_subentry. Pass `entry_id`, `subentry_type`, and `config`. Pass `subentry_id` to reconfigure an existing subentry; omit it to create a new subentry. For flow-type updates, pass the existing entry_id as `helper_id`. Options flows reject the `name` key on update — to rename a flow helper, delete and recreate. Behavior notes: - UPDATE preserves type-specific fields not re-passed (rename never wipes initial/icon/etc. for any simple helper). - Pass `action="create"` or `action="update"` to disambiguate intent. For SIMPLE/FLOW helpers, omitted action falls back to the implicit `helper_id`-presence discriminator. For config subentries, omitted action falls back to the `subentry_id`-presence discriminator. - For flow-based helpers, config keys not declared by any step's data_schema are silently ignored by HA; submit once and the validation error returns the `data_schema` for that helper so subsequent calls use the correct field names. - Validation errors raised by this tool carry the helper's `data_schema` in the response context (and `menu_options` for menu-rooted helpers like `template`/`group` when no sub-type is chosen yet) so a follow-up call can self-correct without a separate schema-discovery round-trip. EXAMPLES (menu-based types + tod, where first-call payload is non-obvious): - template sensor: ha_config_set_helper(helper_type="template", name="Room Temp", config={"next_step_id": "sensor", "state": "{{ states('sensor.x')|float }}", "unit_of_measurement": "°C"}) - group (light): ha_config_set_helper(helper_type="group", name="Kitchen Lights", config={"group_type": "light", "entities": ["light.a", "light.b"]}) - tod (time-of-day indicator, cross-midnight OK): ha_config_set_helper(helper_type="tod", name="Quiet Hours", config={"after_time": "22:00:00", "before_time": "07:00:00"}) - config subentry (create under an existing integration): ha_config_set_helper(helper_type="config_subentry", entry_id="01HXYZ...", subentry_type="conversation", config={"name": "Local agent", "model": "gemma3:27b"}) ``helper-selection.md`` ships in this response under ``skill_content`` by default — decision matrix for picking the right helper type plus worked examples and per-type field tables. For deeper helper-design guidance beyond what ships here, call ha_get_skill_guide.
Parameters
helper_typerequired-(Annotated[Literal['counter', 'config_subentry', 'derivative', 'filter', 'generic_hygrostat', 'generic_thermostat', 'group', 'input_boolean', 'input_button', 'input_datetime', 'input_number', 'input_select', 'input_text', 'integration', 'min_max', 'person', 'random', 'schedule', 'statistics', 'switch_as_x', 'tag', 'template', 'threshold', 'timer', 'tod', 'trend', 'utility_meter', 'zone'], Field(description='Type of helper entity to create or update')])name-(Annotated[str | None, Field(description="Display name for simple/flow helper creation. Required when creating a helper without helper_id. Optional on helper update. Ignored for helper_type='config_subentry', which uses entry_id/subentry_type/subentry_id instead. For flow-based helper updates (template, group, utility_meter, ...), this is typically ignored because options flows don't expose renaming. Rename a flow helper by deleting and recreating instead.", default=None)])= nullhelper_id-(Annotated[str | None, Field(description="REQUIRED when updating an existing helper. Bare ID ('my_button') or full entity ID ('input_button.my_button'). Omit to create a new helper.", default=None)])= nullentry_id-(Annotated[str | None, Field(description="Parent config entry ID when helper_type='config_subentry'. Use ha_get_integration() to find entry IDs.", default=None)])= nullsubentry_type-(Annotated[str | None, Field(description="Integration-defined subentry type when helper_type='config_subentry'.", default=None)])= nullsubentry_id-(Annotated[str | None, Field(description="Existing config subentry ID to reconfigure when helper_type='config_subentry'. Omit to create.", default=None)])= nullshow_advanced_options-(Annotated[bool, Field(description="When helper_type='config_subentry', ask older Home Assistant versions to expose advanced flow options. No-op on HA 2026.6+; pending removal before HA 2027.6.", default=False)])= falseicon-(Annotated[str | None, Field(description="Material Design Icon (e.g., 'mdi:bell', 'mdi:toggle-switch')", default=None)])= nullarea_id-(Annotated[str | None, Field(description='Area/room ID to assign the helper to', default=None)])= nulllabels-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(description='Labels to categorize the helper', default=None)])= nullmin_value-(Annotated[float | None, Field(description="Minimum value (input_number/counter) or minimum length (input_text). Also accepts shorthand 'min'.", default=None, validation_alias=AliasChoices('min_value', 'min'))])= nullmax_value-(Annotated[float | None, Field(description="Maximum value (input_number/counter) or maximum length (input_text). Also accepts shorthand 'max'.", default=None, validation_alias=AliasChoices('max_value', 'max'))])= nullstep-(Annotated[float | None, Field(description='Step/increment value for input_number or counter', default=None)])= nullunit_of_measurement-(Annotated[str | None, Field(description="Unit of measurement for input_number (e.g., '°C', '%', 'W'). Also accepts shorthand 'unit'.", default=None, validation_alias=AliasChoices('unit_of_measurement', 'unit'))])= nulloptions-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(description='List of options for input_select (required for input_select)', default=None)])= nullinitial-(Annotated[str | int | None, Field(description=_INITIAL_PARAM_DESCRIPTION, default=None)])= nullmode-(Annotated[str | None, Field(description="Display mode: 'box'/'slider' for input_number, 'text'/'password' for input_text", default=None)])= nullhas_date-(Annotated[bool | None, Field(description='Include date component for input_datetime', default=None)])= nullhas_time-(Annotated[bool | None, Field(description='Include time component for input_datetime', default=None)])= nullrestore-(Annotated[bool | None, Field(description='Restore state after restart (counter, timer). Defaults to True for counter, False for timer', default=None)])= nullduration-(Annotated[str | None, Field(description="Default duration for timer in format 'HH:MM:SS' or seconds (e.g., '0:05:00' for 5 minutes)", default=None)])= nullmonday-(Annotated[list[dict[str, Any]] | None, JSON_STRING_COERCION, Field(description="Schedule time ranges for Monday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts. Optional 'data' dict for additional attributes (e.g. {'from': '07:00', 'to': '22:00', 'data': {'mode': 'comfort'}})", default=None)])= nulltuesday-(Annotated[list[dict[str, Any]] | None, JSON_STRING_COERCION, Field(description="Schedule time ranges for Tuesday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts. Optional 'data' dict for additional attributes.", default=None)])= nullwednesday-(Annotated[list[dict[str, Any]] | None, JSON_STRING_COERCION, Field(description="Schedule time ranges for Wednesday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts. Optional 'data' dict for additional attributes.", default=None)])= nullthursday-(Annotated[list[dict[str, Any]] | None, JSON_STRING_COERCION, Field(description="Schedule time ranges for Thursday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts. Optional 'data' dict for additional attributes.", default=None)])= nullfriday-(Annotated[list[dict[str, Any]] | None, JSON_STRING_COERCION, Field(description="Schedule time ranges for Friday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts. Optional 'data' dict for additional attributes.", default=None)])= nullsaturday-(Annotated[list[dict[str, Any]] | None, JSON_STRING_COERCION, Field(description="Schedule time ranges for Saturday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts. Optional 'data' dict for additional attributes.", default=None)])= nullsunday-(Annotated[list[dict[str, Any]] | None, JSON_STRING_COERCION, Field(description="Schedule time ranges for Sunday. List of {'from': 'HH:MM', 'to': 'HH:MM'} dicts. Optional 'data' dict for additional attributes.", default=None)])= nulllatitude-(Annotated[float | None, Field(description='Latitude for zone (required for zone)', default=None)])= nulllongitude-(Annotated[float | None, Field(description='Longitude for zone (required for zone)', default=None)])= nullradius-(Annotated[float | None, Field(description='Radius in meters for zone (default: 100)', default=None)])= nullpassive-(Annotated[bool | None, Field(description="Passive zone (won't trigger state changes for person entities)", default=None)])= nulluser_id-(Annotated[str | None, Field(description='User ID to link to person entity', default=None)])= nulldevice_trackers-(Annotated[list[str] | None, JSON_STRING_COERCION, Field(description='List of device_tracker entity IDs for person', default=None)])= nullpicture-(Annotated[str | None, Field(description='Picture URL for person entity', default=None)])= nulltag_id-(Annotated[str | None, Field(description="Tag ID for tag. On create, omit to auto-generate a unique uuid4 hex (HA's tag/create requires this field; the tool fills it in for you). On update, the tag's existing tag_id is required (passed via helper_id).", default=None)])= nulldescription-(Annotated[str | None, Field(description='Description for tag', default=None)])= nullcategory-(Annotated[str | None, Field(description="Category ID to assign to this helper. Use ha_config_get_category(scope='helpers') to list available categories, or ha_config_set_category() to create one.", default=None)])= nullconfig-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION, Field(description="Config dict for flow-based helper types and helper_type='config_subentry' (template, group, utility_meter, derivative, min_max, threshold, integration, statistics, trend, random, filter, tod, generic_thermostat, switch_as_x, generic_hygrostat). Ignored for simple helper types. Field set is delivered as data_schema on the first validation error.", default=None)])= nullwait-(Annotated[bool, Field(description='Wait for helper entity to be queryable before returning. Default: True. Set to False for bulk operations.', default=True)])= trueaction-(Annotated[Literal['create', 'update'] | None, Field(description="Explicit intent: 'create' a new helper or 'update' an existing one. When omitted, falls back to the implicit discriminator: presence of helper_id => update, absence => create. Pass 'create' or 'update' to disambiguate (e.g. so a typo in helper_id surfaces as a clear 'helper not found' error instead of being mistaken for a create call).", default=None)])= nullMandatoryBPS-(Annotated[bool, Field(default=True)])= trueBestPracticeKey-(BestPracticeKeyParam)= nullDescription
Remove a Home Assistant helper or integration config entry. Unifies three backend removal mechanisms — simple-helper websocket delete, config-entry delete, and config-subentry delete — behind one entry point with four routing paths driven by helper_type. WHEN NOT TO USE: - Removing only an entity (without deleting its underlying helper or config entry) — use `ha_remove_entity` instead. - YAML-configured helpers — they have no storage backend. Edit the YAML file and reload the relevant integration. SUPPORTED HELPER TYPES: - SIMPLE (12, websocket-delete): input_button, input_boolean, input_select, input_number, input_text, input_datetime, counter, timer, schedule, zone, person, tag. - FLOW (15, config-entry-delete via entity lookup): template, group, utility_meter, derivative, min_max, threshold, integration, statistics, trend, random, filter, tod, generic_thermostat, switch_as_x, generic_hygrostat. ROUTING: - SIMPLE helper_type + bare helper_id or entity_id → websocket delete. - FLOW helper_type + entity_id → resolve entity_id to config_entry_id via entity_registry, then delete the config entry. All sub-entities (e.g. utility_meter tariffs) are removed together. - helper_type=None + entry_id → direct config entry delete (any integration). - helper_type="config_subentry" + parent entry_id + subentry_id → delete one config subentry. MISSING-TARGET CONTRACT: A target that is *confirmed absent* raises a structured error rather than returning silent success, so a typo'd or stale identifier surfaces immediately at the caller layer (the ``success`` boolean is what agent wrappers branch on). The error code per-path follows the target shape: - SIMPLE (bare helper_id or entity_id): state-machine empty AND entity registry empty → raises ``ENTITY_NOT_FOUND``. - FLOW (entity_id): not in entity registry → raises ``ENTITY_NOT_FOUND``. YAML-configured helpers (no config entry backing) raise ``RESOURCE_NOT_FOUND``. A bare helper_id (no ``.``) on a FLOW target raises ``ENTITY_NOT_FOUND`` — FLOW resolution needs a full entity_id. TOCTOU 404 on the resolved entry_id raises ``RESOURCE_NOT_FOUND``. - Direct config entry (helper_type=None): backend returns HTTP 404 → raises ``RESOURCE_NOT_FOUND``. - Config subentry: backend returns a "not_found" error → raises ``RESOURCE_NOT_FOUND``. Idempotency at the contract level still holds (call N times = same response). Transient connectivity failures (WebSocket disconnected, network timeouts) raise their own codes (``WEBSOCKET_DISCONNECTED``, ``CONNECTION_FAILED``) so retry logic can branch separately. EXAMPLES: - Remove SIMPLE button: ha_remove_helpers_integrations( target="my_button", helper_type="input_button", confirm=True ) - Remove FLOW utility_meter (any sub-entity works): ha_remove_helpers_integrations( target="sensor.energy_peak", helper_type="utility_meter", confirm=True, ) - Remove any integration by entry_id: ha_remove_helpers_integrations( target="01HXYZ...", confirm=True ) - Remove a config subentry: ha_remove_helpers_integrations( target="01HXYZ...", helper_type="config_subentry", subentry_id="subentry-123", confirm=True ) **WARNING:** Removing a helper or integration that is referenced by automations, scripts, or other integrations may cause those to fail. Use ha_search() / ha_get_integration() to verify before removal. Cannot be undone.
Parameters
targetrequired-(Annotated[str, Field(description="What to remove. One of: (a) bare helper_id for SIMPLE helpers (requires helper_type), e.g. 'my_button'; (b) full entity_id (requires helper_type), e.g. 'input_button.my_button' or 'sensor.my_meter'; (c) config entry_id for any integration (helper_type=None), e.g. value from ha_get_integration(); (d) parent config entry_id for config_subentry (requires helper_type='config_subentry' and subentry_id).")])helper_type-(Annotated[HelperTypeLiteral | None, Field(description="Helper type. Required when target is a helper_id (bare) or entity_id. Set to None when target is a config entry_id to remove any integration. Use 'config_subentry' to remove a config subentry under target.", default=None)])= nullsubentry_id-(Annotated[str | None, Field(description="Config subentry ID to remove when helper_type='config_subentry'.", default=None)])= nullconfirm-(Annotated[bool, Field(description='Must be True to confirm removal.', default=False)])= falsewait-(Annotated[bool, Field(description="Wait for entity removal. Default: True. Ignored when helper_type=None or helper_type='config_subentry' (no entity poll, require_restart returned).", default=True)])= trueHistory & Statistics
Description
Retrieve execution traces for automations and scripts to debug issues. Traces show what happened during automation/script runs: - What triggered the automation - Which conditions passed or failed - What actions were executed - Any errors that occurred - Variable values during execution USAGE MODES: 1. List recent traces (omit run_id): ha_get_automation_traces("automation.motion_light") Returns a summary of recent execution runs with timestamps, triggers, and status. Use `offset` to page deeper when `has_more` is true, or `order="oldest"` to start from the earliest stored trace instead of the most recent. 2. Get detailed trace (provide run_id): ha_get_automation_traces("automation.motion_light", run_id="1705312800.123456") Returns full execution details including trigger info, condition results, action trace with timing, and context variables. 3. Get detailed trace with logbook (provide run_id and detailed=True): ha_get_automation_traces("automation.motion_light", run_id="1705312800.123456", detailed=True) Returns the formatted trace plus logbook entries and context metadata. Useful when the standard trace summary doesn't reveal enough for debugging. Note: script-style action paths (sequence/, numeric) are always matched regardless of this flag. 4. Get full variables without deduplication (provide run_id and deduplicate=False): ha_get_automation_traces("automation.motion_light", run_id="1705312800.123456", deduplicate=False) Returns the formatted trace with full variables at every action step. DEBUGGING EXAMPLES: Automation not triggering: - Check if traces exist (automation may not be triggered) - Look at trigger info to see what event was received Automation runs but conditions fail: - Get detailed trace to see condition_results - Each condition shows whether it passed (true) or failed (false) Unexpected behavior in actions: - Get detailed trace to see action_trace - Shows each action step with result and any errors - For 'choose' actions, shows which branch was taken Template debugging: - Detailed trace shows evaluated template values in context - Trigger variables available under trigger_variables NOTES: - Traces are stored for a limited time by Home Assistant - Works for both automations and scripts (use full entity_id) - The 'state' field shows: 'stopped' (completed), 'running', or error state
Parameters
automation_idrequired-(Annotated[str, Field(description="Automation or script entity_id (e.g., 'automation.motion_light' or 'script.morning_routine')")])run_id-(Annotated[str | None, Field(description='Specific trace run_id to retrieve detailed trace. Omit to list recent traces.', default=None)])= nulllimit-(Annotated[int, Field(description='Maximum number of traces to return when listing (default: 10, max: 50).', default=10, ge=1, le=50)])= 10deduplicate-(Annotated[bool, Field(description='Deduplicate variables across action steps (default: True). Set to False to include full variables at every step.', default=True)])= truedetailed-(Annotated[bool, Field(description='Include extra diagnostic data: logbook entries and context metadata (default: False). Use when standard trace lacks detail for debugging.', default=False)])= falsesections-(Annotated[str | None, Field(description="Comma-separated list of trace sections to return. Valid values: trigger, conditions, actions, config, error, logbook, context. Omit to return all sections. Example: 'actions' or 'trigger,conditions'.", default=None)])= nulloffset-(Annotated[int, Field(description='Number of traces to skip from the start of the requested order. Use with `limit` to page through stored traces when `total_available > limit`.', default=0, ge=0)])= 0order-(Annotated[Literal['newest', 'oldest'], Field(description="Order traces are returned in. 'newest' (default) returns most-recent first; 'oldest' returns chronological-first.", default='newest')])= "newest"Description
Retrieve historical data from Home Assistant's recorder. **Sources:** - "history" (default): Raw state changes, ~10 day retention, full resolution - "statistics": Pre-aggregated data, permanent retention, requires state_class **Shared params:** entity_ids, start_time, end_time, limit, offset **History params:** minimal_response, significant_changes_only **Statistics params:** period, statistic_types **Default time range:** 24h for history, 30 days for statistics **Use ha_get_history (default) when:** - Troubleshooting why a value changed ("Why was my bedroom cold last night?") - Checking event sequences ("Did my garage door open while I was away?") - Analyzing recent patterns ("What time does motion usually trigger?") **Use ha_get_history(source="statistics") when:** - Tracking long-term trends beyond 10 days ("Energy use this month vs last month?") - Computing period averages ("Average living room temperature over 6 months?") - Entities must have state_class (measurement, total, total_increasing) **WARNING:** limit and offset apply per entity (not globally across all entities). All data is fetched from HA before slicing; limit/offset are client-side. With multiple entity_ids, offset must be 0 — use a single entity_id for offset > 0. Use has_more and next_offset from the response to paginate. **Example -- history (default):** ```python ha_get_history(entity_ids="sensor.bedroom_temperature", start_time="24h") ha_get_history(entity_ids=["sensor.temperature", "sensor.humidity"], start_time="7d", limit=500) # Default order="desc" returns newest states first. # To paginate oldest-first, use order="asc": ha_get_history(entity_ids="sensor.temperature", start_time="7d", limit=100, offset=100, order="asc") ``` **Example -- statistics:** ```python ha_get_history(source="statistics", entity_ids="sensor.total_energy_kwh", start_time="30d", period="day") ha_get_history(source="statistics", entity_ids="sensor.living_room_temperature", start_time="6m", period="month", statistic_types=["mean", "min", "max"]) ha_get_history(source="statistics", entity_ids="sensor.energy_kwh", start_time="30d", period="5minute", limit=100, offset=200) ```
Parameters
entity_idsrequired-(Annotated[str | list[str], JSON_STRING_COERCION, Field(description='Entity ID(s) to query. Can be a single ID, comma-separated string, or JSON array.')])source-(Annotated[Literal['history', 'statistics'], Field(description='Data source: "history" (default) for raw state changes (~10 day retention), or "statistics" for pre-aggregated long-term data (permanent, requires state_class).', default='history')])= "history"start_time-(Annotated[str | None, Field(description="Start time: ISO datetime or relative (e.g., '24h', '7d', '30d'). Default: 24h ago for history, 30d ago for statistics", default=None)])= nullend_time-(Annotated[str | None, Field(description='End time: ISO datetime. Default: now', default=None)])= nullminimal_response-(Annotated[bool, Field(description='Return only states/timestamps without attributes. Default: true. Ignored when source="statistics"', default=True)])= truesignificant_changes_only-(Annotated[bool, Field(description='Filter to significant state changes only. Default: true. Ignored when source="statistics"', default=True)])= truelimit-(Annotated[int | None, Field(description='Max entries per entity. Default: 100, Max: 1000. For source="history": state changes. For source="statistics": aggregated rows. With multiple entity_ids, offset must be 0 and total rows returned can reach limit × len(entity_ids).', default=None, ge=1, le=1000)])= nulloffset-(Annotated[int | None, Field(description='Number of entries to skip per entity for pagination. Default: 0. Offset > 0 requires a single entity_id. Use with limit and has_more/next_offset in the response.', default=None, ge=0)])= nullperiod-(Annotated[str, Field(description='Aggregation period: "5minute", "hour", "day", "week", "month", "year". Default: "day". Ignored when source="history"', default='day')])= "day"statistic_types-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(description='Statistics types: "mean", "min", "max", "sum", "state", "change". Default: all. Ignored when source="history"', default=None)])= nullorder-(Annotated[Literal['asc', 'desc'], Field(default='desc', description='Sort order for history entries. "desc" (default): newest first. "asc": oldest first (chronological, as returned by HA API). Ignored when source="statistics".')])= "desc"fields-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(default=None, description='Return only the specified top-level response keys to reduce response size. None = full response (default). History keys: success, source, entities, period, query_params. Statistics keys: success, source, entities, period_type, time_range, statistic_types, query_params, warnings.')])= nullDescription
Get Home Assistant logs from various sources. **Sources:** - "logbook" (default): Entity state change history with pagination - "system": Structured system log entries (errors, warnings) via system_log/list - "error_log": Raw home-assistant.log text - "supervisor": Add-on container logs (requires slug = add-on slug) - "system_service": HA-Supervisor-managed system service logs (requires slug ∈ {supervisor, host, core, dns, audio, cli, multicast, observer}) - "logger": Effective log level per integration via logger/log_info (confirms logger.set_level changes took effect) **Shared params:** limit, search (keyword filter on entries/lines; matches integration domain for source='logger') **Order:** order='newest' (default) returns most-recent first; order='oldest' returns chronological-first. Applies to all time-ordered sources (logbook, system, error_log, supervisor, system_service); ignored for source='logger'. For raw-text sources (error_log, supervisor, system_service) it sets the read direction of the most-recent window. **Logbook params:** hours_back, entity_id, end_time, offset, compact (default True — strips attribute dicts to save context) **System/error_log params:** level (ERROR, WARNING, INFO, DEBUG) **Supervisor params:** slug = add-on slug, e.g. "core_mosquitto" (use ha_get_addon() to list installed slugs) **System-service params:** slug = service name. The slug "supervisor" here means the Supervisor service's own logs, NOT an add-on with that name — the source param disambiguates.
Parameters
source-(Literal['logbook', 'system', 'error_log', 'supervisor', 'system_service', 'logger'])= "logbook"limit-(int | None)= nullsearch-(str | None)= nullorder-(Annotated[Literal['newest', 'oldest'], Field(description="Sort order for time-ordered sources (logbook, system, error_log, supervisor, system_service): 'newest' (default) returns most-recent first; 'oldest' returns chronological-first. Ignored for source='logger'.")])= "newest"hours_back-(Annotated[int, Field(ge=1)])= 1entity_id-(str | None)= nullend_time-(str | None)= nulloffset-(Annotated[int, Field(ge=0)])= 0compact-(bool)= truelevel-(str | None)= nullslug-(str | None)= nullIntegrations
Description
Get integration (config entry) information with pagination. Without an entry_id: Lists all configured integrations with optional filters. With an entry_id: Returns detailed information including full options/configuration. EXAMPLES: - List all integrations: ha_get_integration() - Paginate: ha_get_integration(offset=50) - Search: ha_get_integration(query="zigbee") - Get specific entry: ha_get_integration(entry_id="abc123") - Get entry with editable fields: ha_get_integration(entry_id="abc123", include_schema=True) - Get entry with diagnostics dump: ha_get_integration(entry_id="abc123", include_diagnostics=True) - Get device-scoped diagnostics: ha_get_integration(entry_id="abc123", include_diagnostics=True, device_id="dev123") - Get the parsed KNX ETS project (group-address table): ha_get_integration(entry_id="<knx entry>", include_knx_project=True) - Walk a sub-tree: ha_get_integration(entry_id="abc123", include_diagnostics=True, diagnostics_data_path="<dotted-path>") - Paginate a large list: ha_get_integration(entry_id="abc123", include_diagnostics=True, diagnostics_data_path="<list-valued path>", diagnostics_data_limit=10, diagnostics_data_offset=20) - List config subentries: ha_get_integration(entry_id="abc123", include_subentries=True) - Inspect subentry create schema: ha_get_integration(entry_id="abc123", include_subentry_schema=True, subentry_type="conversation") - Inspect subentry reconfigure schema: ha_get_integration(entry_id="abc123", include_subentry_schema=True, subentry_type="conversation", subentry_id="sub123") - List template entries: ha_get_integration(domain="template") STATES: 'loaded', 'setup_error', 'setup_retry', 'not_loaded', 'failed_unload', 'migration_error'. OPTIONS: ``options`` reflect the entry's persisted values; a field that was never set may be absent (rather than shown at its schema default). Values that match a ``secrets.yaml`` entry are returned as ``"**redacted**"``. Use ``include_schema=True`` to see every editable field and its default/type. Nested option *sections* (e.g. a template helper's ``advanced_options``) are additively flattened one level — each section's leaf keys are copied to the top of ``options`` (mirroring the OptionsFlow-derived read) while the raw nested section is preserved for fidelity, and an existing top-level key is never overwritten. Each entry carries: - ``log_level``: the canonical Python logger level name (``DEBUG``/``INFO``/``WARNING``/``ERROR``/``CRITICAL``) when the integration has a ``logger.set_level`` override, or ``"DEFAULT"`` (uppercase sentinel) when no override is set. - ``log_level_raw``: the original numeric level (e.g. ``10`` for DEBUG) when HA returned an int, ``None`` otherwise (no override set, or HA provided a level name as a string). This is distinct from the add-on side, where ``ha_get_addon`` returns Supervisor's lowercase ``"default"`` literal — do not cross-compare.
Parameters
entry_id-(Annotated[str | None, Field(description='Config entry ID to get details for. If omitted, lists all integrations.', default=None)])= nullquery-(Annotated[str | None, Field(description='When listing, search by domain or title. Uses exact substring matching by default; set exact_match=False for fuzzy.', default=None)])= nulldomain-(Annotated[str | None, Field(description="Filter by integration domain (e.g. 'template', 'group'). When set, includes the full options/configuration for each entry.", default=None)])= nullinclude_options-(Annotated[bool, Field(description='Include the options object for each entry. Automatically enabled when domain filter is set. For UI-created flow-based helpers (template, group, utility_meter, derivative, ...), the current config — template body, group members, source entity, etc. — is surfaced here by probing the options flow. Prefer this over include_schema when you only need to read the current values; use include_schema when you also need the field types or selector metadata.', default=False)])= falseinclude_schema-(Annotated[bool, Field(description='When entry_id is set, also return the options flow schema (available fields and their types). Use before ha_config_set_helper to understand what can be updated. Only applies when supports_options=true.', default=False)])= falseinclude_subentries-(Annotated[bool, Field(description='When entry_id is set, include config subentries for the integration entry. Useful for integrations that expose conversation agents, devices, or other extension points as subentries.', default=False)])= falseinclude_subentry_schema-(Annotated[bool, Field(description='When entry_id is set, return introspection-only config subentry schema information; no subentry is created. Pair with subentry_type, and optionally subentry_id for reconfigure schema.', default=False)])= falsesubentry_type-(Annotated[str | None, Field(description='Integration-defined subentry type used with include_subentry_schema=True.', default=None)])= nullsubentry_id-(Annotated[str | None, Field(description='Existing subentry ID used with include_subentry_schema=True to inspect a reconfigure flow.', default=None)])= nullshow_advanced_options-(Annotated[bool, Field(description='When include_subentry_schema=True, ask older Home Assistant versions to expose advanced flow options. No-op on HA 2026.6+; pending removal before HA 2027.6.', default=False)])= falseexact_match-(Annotated[bool, Field(description='Use exact substring matching for query filter (default: True). Set to False for fuzzy matching when the query may contain typos.', default=True)])= truelimit-(Annotated[int, Field(default=50, ge=1, le=200, description='Max entries to return per page in list mode (default: 50)')])= 50offset-(Annotated[int, Field(default=0, ge=0, description='Number of entries to skip for pagination (default: 0)')])= 0include_diagnostics-(Annotated[bool, Field(description="When entry_id is set, also fetch the integration's diagnostics dump — integration-defined JSON (commonly includes redacted config, device list, state snapshots; exact top-level keys vary by integration). The canonical artifact users grab via Settings → Devices & Services → [integration] → ⋯ → Download diagnostics. Use when triaging integration bugs or filing ha_report_issue for a specific integration. Payloads can be large (Hue ~290 KB, ZHA/MQTT/ESPHome several MB) — pair with diagnostics_fields or diagnostics_truncate_at_bytes to fit the LLM context budget.", default=False)])= falseinclude_knx_project-(Annotated[bool, Field(description='When entry_id is a KNX config entry, also return the parsed ETS project: the full group-address table (address, name, DPT, description) under knx_project.group_addresses, plus the group-range hierarchy and project metadata. This is the parsed-project GA table that is NOT in the diagnostics dump; per-entity GA assignments are already covered by include_diagnostics (config_store / configuration_yaml). Ignored (with a warning) when the entry is not a KNX integration. The KNX integration exposes a single project, so the result is the same regardless of which KNX entry_id is used.', default=False)])= falsedevice_id-(Annotated[str | None, Field(description='Optional. When set with include_diagnostics=True, returns the device-scoped diagnostics dump for that specific device under the integration (rather than the full integration dump). Some integrations only expose config-entry-level dumps; others expose both.', default=None)])= nulldiagnostics_fields-(Annotated[list[str] | str | None, JSON_STRING_COERCION, Field(description="Optional list of top-level keys to keep from the diagnostics data payload (e.g. ['home_assistant', 'issues']). Trims the payload before it hits the LLM context budget. Accepts a JSON list or comma-separated string. Only applies when include_diagnostics=True and the data payload is a dict. Unknown keys are silently dropped and surfaced via the omitted_fields sub-key.", default=None)])= nulldiagnostics_truncate_at_bytes-(Annotated[int | None, Field(description='Optional byte cap on the serialized diagnostics payload (after diagnostics_fields and diagnostics_data_path have been applied). On hit, drops data and emits truncated=true, bytes_total, byte_cap, plus available_fields (when the capped value is a dict). Recommended starting point: 20000 bytes. Only applies when include_diagnostics=True.', default=None, ge=1)])= nulldiagnostics_data_path-(Annotated[str | None, Field(description="Optional dotted path into the diagnostics data sub-tree (e.g. '<list-valued path>' for per-device records, 'home_assistant.version' for HA core version; the exact key path varies by integration version). Walks into the post-fields payload. Resolution failures replace data with null and surface data_path_error. Use this when the interesting payload lives several levels deep — top-level diagnostics_fields can't address sub-trees on integrations where the bulk lives under one key (ZHA, MQTT, ESPHome). Only applies when include_diagnostics=True.", default=None)])= nulldiagnostics_data_offset-(Annotated[int | None, Field(description='Pagination start index (default 0) for list-valued diagnostics_data_path results. Ignored when diagnostics_data_path is unset, diagnostics_data_limit is unset, or the resolved value is not a list. Only applies when include_diagnostics=True.', default=0, ge=0)])= 0diagnostics_data_limit-(Annotated[int | None, Field(description='Pagination window size for list-valued diagnostics_data_path results. When set with a list-resolved path, swaps data for a pagination envelope {path, items, offset, limit, total, has_more}. Default None returns the full resolved value. Workflow: probe with a list-valued diagnostics_data_path and diagnostics_data_limit=10 to walk a large list one page at a time (the exact path varies by integration version). Only applies when include_diagnostics=True.', default=None, ge=1)])= nullDescription
Get Home Assistant system health, including Zigbee (ZHA), Z-Wave JS, and per-integration diagnostics dumps. Returns health check results from integrations, system resources, and connectivity. Available information varies by installation type and loaded integrations. The result also carries an ``ha_mcp_update`` object — ``{current, latest, update_available}`` — reporting whether a newer ha-mcp release is available (from PyPI for pip/Docker, or the Supervisor add-on store for the add-on), so you can proactively tell the user to upgrade. Present on every install type including the HA add-on (so a user who missed the Supervisor's update prompt still hears about it); omitted only for the ``unknown`` version and when ``HA_MCP_DISABLE_UPDATE_CHECK`` is set. **Parameters:** - include: Optional comma-separated list of additional data to include. - "repairs": Repair items from Settings > System > Repairs (active only by default; pass `include_dismissed_repairs=True` for all). To dismiss/ignore a repair, call `ha_call_service(ws_command="repairs/ignore_issue", data={"domain": <domain>, "issue_id": <issue_id>, "ignore": true})`. - "zha_network": ZHA Zigbee devices with radio signal summary (name, LQI, RSSI) - "zha_network_full": ZHA Zigbee devices with all device details (can be large on 100+ device networks; prefer "zha_network" for summary) - "zwave_network": Z-Wave JS network status and node summary (status, security, routing) - "thread_network": Thread/OpenThread Border Router (OTBR) summary — per border-router channel, extended_pan_id, and border_agent_id (integration-presence + radio-network view, not per-node Thread health) - "matter_network": Matter integration presence summary — config_entry_id, state, and title (per-node health is exposed separately via Matter node diagnostics, not here) - "themes": Installed theme names and defaults (sorted list of theme names, count, default_theme, default_dark_theme) - "diagnostics": Per-integration diagnostics dump — integration-defined JSON (commonly includes redacted config, device list, state snapshots; exact top-level keys vary by integration). REQUIRES ``config_entry_id``. The canonical artifact users grab via Settings → Devices & Services → [integration] → ⋯ → Download diagnostics. Use this when triaging integration bugs or filing ``ha_report_issue`` for a specific integration. Payloads can be large (Hue ~290 KB, ZHA/MQTT/ESPHome several MB) — pair with ``diagnostics_fields`` or ``diagnostics_truncate_at_bytes`` to fit the LLM context budget. - "config_check": Validate HA configuration via POST /config/core/check_config (the pre-restart safety check; ha_restart runs it automatically). Returns {result: valid|invalid, is_valid, errors}; read-only/idempotent, takes no args. - "dead_entities": Surface orphaned/stale entity-registry entries by diffing the registry against the state machine and the live config-entries set. Returns confidence-tiered buckets — ``config_entry_orphans`` (owning integration instance gone; definitively dead) and ``stale_restored`` (HA restored the entity from the registry on startup but the loaded integration no longer provides it). Each item carries entity_id + platform so a client can propose cleanup with ha_remove_entity. Deliberately excludes ``unknown``-state entities and merely-offline devices to keep false positives low. Read-only; takes no args. - Example: include="repairs,zha_network,zwave_network,config_check" - Example: include="diagnostics", config_entry_id="abc123..." - include_dismissed_repairs: Include user-dismissed/ignored repairs (default: False). Only meaningful when "repairs" is in `include`. - config_entry_id: Required when ``include`` contains ``diagnostics``. The config entry ID of the integration (find via ``ha_get_integration``). - device_id: Optional. When set with ``include=diagnostics``, returns the device-scoped diagnostics dump for that specific device under the integration (rather than the full integration dump). Some integrations only expose config-entry-level dumps; others expose both. - diagnostics_fields: Optional list of top-level keys to keep from the diagnostics ``data`` payload (e.g. ``["home_assistant", "issues"]``). Accepts a JSON list or comma-separated string. Only applies with ``include=diagnostics``. - diagnostics_truncate_at_bytes: Optional byte cap on the serialized diagnostics payload (post-projection / post-data_path). On hit, drops ``data`` and emits ``truncated=true``, ``bytes_total``, ``byte_cap``, plus ``available_fields`` (when the capped value is a dict). Only applies when ``include`` contains ``diagnostics``. Recommended starting point: 20000 bytes. - diagnostics_data_path: Optional dotted path into the diagnostics ``data`` sub-tree (e.g. ``"data.devices"`` for ZHA per-device records). Walks into the post-fields payload. Resolution failures replace ``data`` with ``null`` and surface ``data_path_error``. Only applies when ``include`` contains ``diagnostics``. - diagnostics_data_offset / diagnostics_data_limit: Pagination on list-valued ``diagnostics_data_path`` results. When ``data_limit`` is set and the resolved path is a list, ``data`` becomes ``{"path", "items", "offset", "limit", "total", "has_more"}``. Only applies when ``include`` contains ``diagnostics``. Example workflow (walk a list-valued sub-tree one page at a time; the exact ``data_path`` varies by integration version): ``ha_get_system_health(include="diagnostics", config_entry_id="abc", diagnostics_data_path="<list-valued path>", diagnostics_data_limit=10)`` → inspect the page envelope's ``total`` / ``has_more`` → repeat with ``diagnostics_data_offset=10`` for the next slice.
Parameters
include-(str | None)= nullinclude_dismissed_repairs-(bool | None)= falseconfig_entry_id-(str | None)= nulldevice_id-(str | None)= nulldiagnostics_fields-(Annotated[list[str] | str | None, JSON_STRING_COERCION])= nulldiagnostics_truncate_at_bytes-(Annotated[int, Field(ge=1)] | None)= nulldiagnostics_data_path-(str | None)= nulldiagnostics_data_offset-(Annotated[int, Field(ge=0)] | None)= 0diagnostics_data_limit-(Annotated[int, Field(ge=1)] | None)= nullDescription
Manage an integration (config entry): enable/disable, add, or update options. Modes (pick one): - Enable/disable: entry_id + enabled. - Add integration: domain (+ config) — drives the domain's config flow, including menus and multi-step forms. - Update options: entry_id + config — drives the entry's options flow (what the "Configure" button does in the HA UI). WHEN NOT TO USE: - Helpers (template, group, utility_meter, ...): use ha_config_set_helper. - Config subentries: use ha_config_set_helper(helper_type='config_subentry'). - Removing an entry: use ha_remove_helpers_integrations. Use ha_get_integration() to find entry IDs, and ha_get_integration(entry_id=..., include_schema=True) to inspect the options fields before an update. Caveats: adding an integration runs its config flow exactly as the HA UI would (may pair devices, scan the network, create entities). Flows requiring a browser step (OAuth) or an asynchronous provider step error out at that step with a structured error instead of completing. EXAMPLES: - Disable: ha_set_integration(entry_id="abc123", enabled=False) - Add: ha_set_integration(domain="workday", config={"name": "Workday"}) - Update options: ha_set_integration(entry_id="abc123", config={"scan_interval": 30})
Parameters
entry_id-(Annotated[str | None, Field(description="Config entry ID of an existing integration (enable/disable and options-update modes). Omit when adding via 'domain'.", default=None)])= nullenabled-(Annotated[bool | None, Field(description="True to enable, False to disable the entry. Requires entry_id; mutually exclusive with 'domain' and 'config'.", default=None)])= nulldomain-(Annotated[str | None, Field(description="Integration domain to add (e.g. 'workday', 'local_calendar') — starts and drives that domain's config flow. Pass the flow's form fields in 'config'.", default=None)])= nullconfig-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION, Field(description="Flow form data. With 'domain': input for the new integration's config flow. With 'entry_id' alone: input for the entry's options flow (updates its options). Multi-step flows consume keys per step; menu steps take 'next_step_id'. The step's data_schema is returned on validation errors so field names can be corrected.", default=None)])= nullLabels & Categories
Description
Get category info - list all categories for a scope or get a specific one by ID. Without a category_id: Lists all Home Assistant categories for the given scope. With a category_id: Returns configuration for that specific category. Categories are domain-scoped organizational groups for automations, scripts, scenes, and helpers. CATEGORY PROPERTIES: - ID (category_id), Name - Icon (optional) EXAMPLES: - List automation categories: ha_config_get_category("automation") - List script categories: ha_config_get_category("script") - List helper categories: ha_config_get_category("helpers") - Get specific category: ha_config_get_category("automation", category_id="my_category_id") Use ha_config_set_category() to create or update categories. Use ha_set_entity(categories={"automation": "category_id"}) to assign categories to entities.
Parameters
scoperequired-(Annotated[str, Field(description="Domain scope for categories (e.g., 'automation', 'script', 'scene', 'helpers').")])category_id-(Annotated[str | None, Field(description='ID of the category to retrieve. If omitted, lists all categories for the scope.', default=None)])= nullDescription
Get label info - list all labels or get a specific one by ID. Without a label_id: Lists all Home Assistant labels with their configurations. With a label_id: Returns configuration for that specific label. LABEL PROPERTIES: - ID (label_id), Name - Color (optional), Icon (optional), Description (optional) EXAMPLES: - List all labels: ha_config_get_label() - Get specific label: ha_config_get_label("my_label_id") Use ha_config_set_label() to create or update labels. Use ha_set_entity(labels=["label1", "label2"]) to assign labels to entities.
Parameters
label_id-(Annotated[str | None, Field(description='ID of the label to retrieve. If omitted, lists all labels.', default=None)])= nullDescription
Delete a Home Assistant category. Removes the category from the category registry for the given scope (e.g., 'automation', 'script', 'scene', 'helpers'). This will also remove the category assignment from all entities in that scope. EXAMPLES: - Delete category: ha_config_remove_category("automation", "my_category_id") Use ha_config_get_category() to find category IDs. **WARNING:** Deleting a category will remove it from all assigned entities. This action cannot be undone.
Parameters
scoperequired-(Annotated[str, Field(description="Domain scope for the category (e.g., 'automation', 'script', 'scene', 'helpers').")])category_idrequired-(Annotated[str, Field(description='ID of the category to delete')])Description
Delete a Home Assistant label. Removes the label from the label registry. This will also remove the label from all entities, devices, and areas that have it assigned. EXAMPLES: - Delete label: ha_config_remove_label("my_label_id") Use ha_config_get_label() to find label IDs. **WARNING:** Deleting a label will remove it from all assigned entities. This action cannot be undone.
Parameters
label_idrequired-(Annotated[str, Field(description='ID of the label to delete')])Description
Create or update a Home Assistant category. Creates a new category if category_id is not provided, or updates an existing category if category_id is provided. Categories are domain-scoped organizational groups for automations, scripts, scenes, and helpers. Unlike labels (which are cross-domain), categories are specific to a single domain scope. EXAMPLES: - Create automation category: ha_config_set_category("Lighting", scope="automation") - Create with icon: ha_config_set_category("Security", scope="automation", icon="mdi:shield") - Update category: ha_config_set_category("Updated Name", scope="automation", category_id="my_category_id") After creating a category, use ha_set_entity(categories={"automation": "category_id"}) to assign it.
Parameters
namerequired-(Annotated[str, Field(description='Display name for the category')])scoperequired-(Annotated[str, Field(description="Domain scope for the category (e.g., 'automation', 'script', 'scene', 'helpers').")])category_id-(Annotated[str | None, Field(description='Category ID for updates. If not provided, creates a new category.', default=None)])= nullicon-(Annotated[str | None, Field(description="Material Design Icon (e.g., 'mdi:tag', 'mdi:label')", default=None)])= nullDescription
Create or update a Home Assistant label. Creates a new label if label_id is not provided, or updates an existing label if label_id is provided. Labels are a flexible tagging system that can be applied to entities, devices, and areas for organization and automation purposes. EXAMPLES: - Create simple label: ha_config_set_label("Critical") - Create colored label: ha_config_set_label("Outdoor", color="green") - Create label with icon: ha_config_set_label("Battery Powered", icon="mdi:battery") - Create full label: ha_config_set_label("Security", color="red", icon="mdi:shield", description="Security-related devices") - Update label: ha_config_set_label("Updated Name", label_id="my_label_id", color="blue") After creating a label, use ha_set_entity(labels=["label_id"]) to assign it to entities.
Parameters
namerequired-(Annotated[str, Field(description='Display name for the label')])label_id-(Annotated[str | None, Field(description='Label ID for updates. If not provided, creates a new label.', default=None)])= nullcolor-(Annotated[str | None, Field(description="Color for the label (e.g., 'red', 'blue', 'green', or hex like '#FF5733')", default=None)])= nullicon-(Annotated[str | None, Field(description="Material Design Icon (e.g., 'mdi:tag', 'mdi:label')", default=None)])= nulldescription-(Annotated[str | None, Field(description="Description of the label's purpose", default=None)])= nullMatter
Description
Manage Home Assistant radios — Z-Wave, Zigbee, Matter, and Thread. For read-only inspection prefer ha_get_device / ha_get_system_health, which mirror the 'diagnostics' and 'network_status' actions; use this tool for writes and the active 'ping' probe (unique to this tool). Write actions perform inclusion/commissioning, removal, healing, reconfiguration, firmware updates and credential provisioning. Caveats: destructive actions (e.g. remove_device, network restore, change_channel, hard_reset, remove_fabric) require confirm=True. Long-running actions (inclusion, rebuild routes, firmware) start the operation and return immediately with long_running=true; completion happens out-of-band. Interactive Z-Wave S2 secure inclusion (read-the- PIN pairing) is not scriptable — use SmartStart/QR provisioning here or the HA UI.
Parameters
radiorequired-(Annotated[Literal['zwave', 'zigbee', 'matter', 'thread'], Field(description='Which radio to manage.')])actionrequired-(Annotated[str, Field(description="Operation to perform. Actions vary per radio; an unknown action returns the supported list for that radio. Common: 'diagnostics', 'network_status', 'ping', 'add'/'commission', 'remove_device', 'reinterview'/'reconfigure', 'firmware_update'.")])device_id-(Annotated[str | None, Field(description='Target device (node) for node-scoped actions.', default=None)])= nullentity_id-(Annotated[str | None, Field(description='Resolve the device from this entity for node-scoped actions.', default=None)])= nullparams-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION, Field(description="Action-specific parameters (e.g. code, pin, channel, property, value). An unknown action returns that radio's supported action list with one-line summaries.", default=None)])= nullconfirm-(Annotated[bool, Field(description='Required (True) to run destructive actions.', default=False)])= falseScenes
Description
Retrieve Home Assistant scene configuration. Returns the complete configuration for a scene, including the ``entities`` dict and other settings (``name``, ``icon``, ``id``). EXAMPLES: - Get scene: ha_config_get_scene("movie_night") - Get scene: ha_config_get_scene("bedroom_dim") RELATED TOOLS: - ha_config_set_scene — pass the returned ``config_hash`` for ``python_transform`` updates. For detailed scene configuration help, use ha_get_skill_guide.
Parameters
scene_idrequired-(Annotated[str, Field(description="Scene identifier (e.g., 'movie_night')")])Description
Delete a Home Assistant scene. EXAMPLES: - Delete scene: ha_config_remove_scene("old_scene") - Delete scene: ha_config_remove_scene("temporary_scene") **IMPORTANT LIMITATION:** This tool can only delete scenes created via the Home Assistant UI. Scenes defined in YAML configuration files (scenes.yaml or configuration.yaml) cannot be deleted through the API and will return a 405 Method Not Allowed error. To remove YAML-defined scenes, you must edit the configuration file directly. **WARNING:** Deleting a scene that is referenced by automations or scripts (via ``scene.turn_on``) may cause those to fail.
Parameters
scene_idrequired-(Annotated[str, Field(description="Scene identifier to delete (e.g., 'old_scene')")])wait-(Annotated[bool, Field(description='Wait for scene to be fully removed before returning. Default: True.', default=True)])= trueDescription
Create or update a Home Assistant scene. MUST call ha_get_skill_guide OR refer to your locally installed skills first. Supports two modes: full config replacement (``config``) or Python transformation of an existing scene (``python_transform``). See the field descriptions for ``python_transform`` examples and the ``config`` shape contract. WHEN TO USE: - ``python_transform``: surgical edits to an existing scene (add/remove/update a single entity entry). Requires ``config_hash`` from ha_config_get_scene() for optimistic locking. - ``config``: creating a new scene, or wholesale replacement. WHEN NOT TO USE: - To activate a scene at runtime, use ha_call_service(domain="scene", service="turn_on", target=...) — this tool only manages scene *configuration*, not the runtime turn-on/off side. - To list or look up existing scenes, use ha_search(domain_filter="scene"). SCENE SHAPE: ``entities`` is a dict keyed by entity_id (e.g., ``{'light.kitchen': {'state': 'on', 'brightness': 200}}``), NOT a list. Automations use a list of actions; scenes capture a snapshot of states as a dict. EXAMPLE: ha_config_set_scene(scene_id="movie_night", config={ "name": "Movie Night", "entities": { "light.living_room": {"state": "on", "brightness": 50}, }, "icon": "mdi:movie", }) The top-level ``SKILL.md`` for home-assistant-best-practices ships in this response under ``skill_content`` by default — generic best-practice index covering entity-naming and safe-refactoring patterns that intersect with scene authoring. For detailed scene configuration help beyond that, use ha_get_skill_guide.
Parameters
scene_idrequired-(Annotated[str, Field(description="Scene identifier (e.g., 'movie_night')")])config-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION, Field(description="Scene configuration dictionary. Must include 'entities' (a dict keyed by entity_id, NOT a list). Optional fields: 'name' (defaults to scene_id), 'icon', 'id'. Mutually exclusive with python_transform.", default=None)])= nullpython_transform-(Annotated[str | None, Field(description='Python expression to transform existing scene config. Mutually exclusive with config. Requires config_hash for validation. WARNING: Expressions with infinite loops will hang the server. Examples: Add entity: python_transform="config[\'entities\'][\'light.bed\'] = {\'state\': \'on\'}" Update brightness: python_transform="config[\'entities\'][\'light.kitchen\'][\'brightness\'] = 50" Remove entity: python_transform="del config[\'entities\'][\'light.kitchen\']" \n\n' + get_security_documentation())])= nullconfig_hash-(Annotated[str | None, Field(description='Config hash from ha_config_get_scene for optimistic locking. REQUIRED for python_transform (validates scene unchanged). Optional for config updates (validates before full replacement if provided).')])= nullcategory-(Annotated[str | None, Field(description="Category ID to assign to this scene. Use ha_config_get_category(scope='scene') to list available categories, or ha_config_set_category() to create one.", default=None)])= nullwait-(Annotated[bool, Field(description='Wait for scene to be queryable before returning. Default: True. Set to False for bulk operations.', default=True)])= trueMandatoryBPS-(Annotated[bool, Field(default=True)])= trueBestPracticeKey-(BestPracticeKeyParam)= nullScripts
Description
Retrieve Home Assistant script configuration. Returns the complete configuration for a script, including sequence, mode, fields, and other settings. The returned `config_hash` is stable across consecutive reads of an unchanged config — `compute_config_hash` documents the underlying contract. The returned `script_id` is the canonical bare storage key resolved by the REST client (matching what `ha_config_set_script` / `ha_config_remove_script` expect), falling back to the input identifier on the rare path where the REST envelope omits it. A leading `script.` prefix on the input is stripped before lookup — behavioral parity with `ha_config_get_automation` (mechanism differs: automations resolve via state lookup; scripts strip the prefix). EXAMPLES: - Get script (bare form): ha_config_get_script("morning_routine") - Get script (entity_id form): ha_config_get_script("script.morning_routine") For detailed script configuration help, use ha_get_skill_guide.
Parameters
script_idrequired-(Annotated[str, Field(description="Script identifier — bare storage key ('morning_routine') or entity_id form ('script.morning_routine'); a leading 'script.' prefix is stripped before lookup.")])Description
Delete a Home Assistant script. EXAMPLES: - Delete script: ha_config_remove_script("old_script") - Delete script: ha_config_remove_script("temporary_script") **IMPORTANT LIMITATION:** This tool can only delete scripts created via the Home Assistant UI. Scripts defined in YAML configuration files (scripts.yaml or configuration.yaml) cannot be deleted through the API and will return a 405 Method Not Allowed error. To remove YAML-defined scripts, you must edit the configuration file directly. **WARNING:** Deleting a script that is used by automations may cause those automations to fail.
Parameters
script_idrequired-(Annotated[str, Field(description="Script identifier to delete — bare storage key ('old_script') or entity_id form ('script.old_script'); a leading 'script.' prefix is stripped before lookup.")])wait-(Annotated[bool, Field(description='Wait for script to be fully removed before returning. Default: True.', default=True)])= trueDescription
Create or update a Home Assistant script. MUST call ha_get_skill_guide OR refer to your locally installed skills first. PREFER NATIVE ACTIONS OVER TEMPLATES (read this before writing any `{{ ... }}`): Native actions are validated at config load, fail loudly, and do not bypass HA's schema. Templates in logic positions fail silently and obscure intent. - `choose` / `if/then/else` instead of template-based service names - `wait_for_trigger` instead of `wait_template` - Native `for:` field on `state` conditions inside `choose`/`if`, and on `state`/`numeric_state` triggers in `wait_for_trigger`, instead of `{{ now() - X.last_changed > timedelta(...) }}` duration math. - `repeat` with `for_each` instead of template loops - Hardcode `target.entity_id` literals — never `{{ this.entity_id }}`. Templates are appropriate ONLY in `data.*` fields, notification message/title, `event_data`, and `variables`. The reactive best-practice checker on this tool will surface anything in a logic position that should be native; consult the `best_practice_warnings` field on the response and fix before re-submitting. The relevant skill section is auto-embedded under `skill_content` on warnings, and the full `automation-patterns.md` + `template-guidelines.md` references ship under `skill_content` proactively by default. For comprehensive guidance beyond that, call `ha_get_skill_guide`. Supports two modes: full config replacement OR Python transformation. WHEN TO USE WHICH MODE: - python_transform: RECOMMENDED for edits to existing scripts. Surgical updates. - config: Use for creating new scripts or full restructures. IMPORTANT: python_transform requires 'config_hash' from ha_config_get_script(). PYTHON TRANSFORM EXAMPLES: - Update step: python_transform="config['sequence'][0]['data']['message'] = 'Hello'" - Add step: python_transform="config['sequence'].append({'delay': {'seconds': 5}})" - Remove last step: python_transform="config['sequence'].pop()" Creates a new script or updates an existing one with the provided configuration. Supports both regular scripts (with sequence) and blueprint-based scripts. Required config fields (choose one): - sequence: List of actions to execute (for regular scripts) - use_blueprint: Blueprint configuration (for blueprint-based scripts) Optional config fields: - alias: Display name (defaults to script_id) - description: Script description - icon: Icon to display - mode: Execution mode ('single', 'restart', 'queued', 'parallel') - max: Maximum concurrent executions (for queued/parallel modes) - fields: Input parameters for the script SCRIPTS vs AUTOMATIONS: Scripts use 'sequence', NOT 'trigger' or 'action'. If you need trigger-based execution, use ha_config_set_automation instead. EXAMPLES: Create basic delay script: ha_config_set_script(script_id="wait_script", config={ "sequence": [{"delay": {"seconds": 5}}], "alias": "Wait 5 Seconds", "description": "Simple delay script" }) Create service call script: ha_config_set_script(script_id="blink_light", config={ "sequence": [ {"action": "light.turn_on", "target": {"entity_id": "light.living_room"}}, {"delay": {"seconds": 2}}, {"action": "light.turn_off", "target": {"entity_id": "light.living_room"}} ], "alias": "Light Blink", "mode": "single" }) Create script with parameters: ha_config_set_script(script_id="backup_script", config={ "alias": "Backup with Reference", "description": "Create backup with optional reference parameter", "fields": { "reference": { "name": "Reference", "description": "Optional reference for backup identification", "selector": {"text": None} } }, "sequence": [ { "action": "hassio.backup_partial", "data": { "compressed": False, "homeassistant": True, "homeassistant_exclude_database": True, "name": "Backup_{{ reference | default('auto') }}_{{ now().strftime('%Y%m%d_%H%M%S') }}" } } ] }) Update script: ha_config_set_script(script_id="morning_routine", config={ "sequence": [ {"action": "light.turn_on", "target": {"area_id": "bedroom"}}, {"action": "climate.set_temperature", "target": {"entity_id": "climate.bedroom"}, "data": {"temperature": 22}} ], "alias": "Updated Morning Routine" }) Create blueprint-based script: ha_config_set_script(script_id="notification_script", config={ "alias": "My Notification Script", "use_blueprint": { "path": "notification_script.yaml", "input": { "message": "Hello World", "title": "Test Notification" } } }) Update blueprint script inputs: ha_config_set_script(script_id="notification_script", config={ "alias": "My Notification Script", "use_blueprint": { "path": "notification_script.yaml", "input": { "message": "Updated message", "title": "Updated Title" } } }) Note: Scripts use Home Assistant's action syntax. Check the documentation for advanced features like conditions, variables, parallel execution, and service call options.
Parameters
script_idrequired-(Annotated[str, Field(description="Script identifier — bare storage key ('morning_routine') or entity_id form ('script.morning_routine'); a leading 'script.' prefix is stripped before lookup.")])config-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION, Field(description="Script configuration dictionary. Must include EITHER 'sequence' (for regular scripts) OR 'use_blueprint' (for blueprint-based scripts). Optional fields: 'alias', 'description', 'icon', 'mode', 'max', 'fields'. Mutually exclusive with python_transform.", default=None)])= nullpython_transform-(Annotated[str | None, Field(description='Python expression to transform existing script config. Mutually exclusive with config. Requires config_hash for validation. WARNING: Expressions with infinite loops will hang the server. Examples: Simple: python_transform="config[\'sequence\'][0][\'data\'][\'message\'] = \'Hello\'" Pattern: python_transform="for step in config[\'sequence\']: if step.get(\'alias\') == \'My Step\': step[\'data\'][\'value\'] = 100" \n\n' + get_security_documentation())])= nullconfig_hash-(Annotated[str | None, Field(description='Config hash from ha_config_get_script for optimistic locking. REQUIRED for python_transform (validates script unchanged). Optional for config updates (validates before full replacement if provided).')])= nullcategory-(Annotated[str | None, Field(description="Category ID to assign to this script. Use ha_config_get_category(scope='script') to list available categories, or ha_config_set_category() to create one.", default=None)])= nullwait-(Annotated[bool, Field(description='Wait for script to be queryable before returning. Default: True. Set to False for bulk operations.', default=True)])= trueMandatoryBPS-(Annotated[bool, Field(default=True)])= trueBestPracticeKey-(BestPracticeKeyParam)= nullSearch & Discovery
Description
Get AI-friendly system overview with intelligent categorization. Returns comprehensive system information at the requested detail level, including Home Assistant base_url, version, location, timezone, entity overview, and active persistent notifications (if any). Use 'minimal' (default) for most queries. Domain counts and states_summary are always complete regardless of entity pagination. Standard/full modes paginate entities (default 200 per page) — use offset to fetch more. Use 'domains' filter to narrow scope. Use fields= to project the response to only the keys you need — a significantly smaller payload when fetching a single sub-section (e.g. fields=["system_info"] returns just that section instead of the full overview). When (and only when) the ha-mcp settings-UI sidecar is running (stdio mode, e.g. Claude Desktop / Claude Code), the response includes a ``settings_url`` field — the local URL to the tool-configuration page. Hand this URL to the user when they ask how to enable or disable tools or change server settings. ``settings_url`` is emitted regardless of ``fields=`` projection (so it stays discoverable even when callers minimize the response) but only when the sidecar URL file actually exists. In HTTP / Docker / OAuth modes there is no sidecar URL file and the server can't know its externally reachable host, so the response instead carries a ``settings_url_hint`` string telling the user where the page is mounted and to read the full URL from the startup logs. Hand whichever of the two fields is present to the user. The response also carries an ``ha_mcp_update`` object ``{current, latest, update_available}`` reporting whether a newer ha-mcp release is available (PyPI for pip/Docker, the Supervisor add-on store for the add-on) — proactively tell the user when ``update_available`` is true. Emitted regardless of ``fields=``; omitted only for the ``unknown`` version and when ``HA_MCP_DISABLE_UPDATE_CHECK`` is set.
Parameters
detail_level-(Annotated[Literal['minimal', 'standard', 'full'], Field(default='minimal', description="'minimal': 10 entities/domain, top-5 states (default); 'standard': 200 entities/page, top-10 states (use offset for more); 'full': 200 entities/page + entity_id + state + full states. Use 'domains', 'limit', or max_entities_per_domain to control size")])= "minimal"domains-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(default=None, description="Filter to specific domains (e.g. 'light,sensor' or ['light','sensor']). None = all domains. Useful to avoid context window overload.")])= nulllimit-(Annotated[int | None, Field(default=None, ge=1, description='Max total entities across all domains (default: unlimited for minimal, 200 for standard/full). Counts and states always complete. Use with offset for pagination.')])= nulloffset-(Annotated[int, Field(default=0, ge=0, description='Number of entities to skip for pagination (default: 0)')])= 0max_entities_per_domain-(Annotated[int | None, Field(default=None, description='Override default entity cap per domain (minimal=10, standard/full=unlimited). 0 = no limit on entities or states.')])= nullinclude_state-(Annotated[bool | None, Field(default=None, description='Include state field for entities (None = auto based on level). Full defaults to True.')])= nullinclude_entity_id-(Annotated[bool | None, Field(default=None, description='Include entity_id field for entities (None = auto based on level). Full defaults to True.')])= nullinclude_notifications-(Annotated[bool | None, Field(default=True, description='Include active persistent notifications (default: True). Set False to skip.')])= trueinclude_dismissed_repairs-(Annotated[bool | None, Field(default=False, description='Include user-dismissed/ignored repairs (default: False). Matches the HA Repairs UI which hides dismissed items by default. To dismiss/ignore a repair, call ha_call_service with ws_command="repairs/ignore_issue" and data={"domain": ..., "issue_id": ..., "ignore": true}.')])= falsefields-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(default=None, description='Return only the specified top-level response keys to reduce response size (e.g. ["system_info", "domains"]). None = full response (default). Available keys: success, system_summary, domain_stats, area_analysis, ai_insights, pagination, partial, warnings, device_types, service_availability, system_info, notification_count, notifications, repair_count, dismissed_repair_count, repairs, repairs_error, tool_discovery, settings_url, settings_url_hint, read_only_mode, read_only_mode_hint, ha_mcp_update. Note: ``settings_url`` (stdio mode), ``settings_url_hint`` (HTTP/Docker/OAuth mode), the ``read_only_mode`` / ``read_only_mode_hint`` pair (only while Read Only Mode is on), and ``ha_mcp_update`` (when an update check applies) are emitted regardless of ``fields=`` projection so the settings page, the active mode, and a newer ha-mcp release stay discoverable; see the tool description.')])= nullDescription
Get current status, state, and attributes of one or more entities (lights, switches, sensors, climate, covers, locks, fans, etc.). SINGLE ENTITY: Pass a string entity_id. Returns the entity's full state and attributes. MULTIPLE ENTITIES: Pass a list of entity IDs (max 100). Efficiently retrieves states using parallel requests. Duplicates are automatically deduplicated. Returns success=True if at least one entity state was retrieved. Check 'error_count' for any failed lookups in partial-success scenarios. FIELDS PROJECTION: `fields=` projects the per-entity record keys (see the fields= parameter description for the full key list), NOT the outer bulk response wrapper. In single-entity mode it filters keys of the returned record directly. In bulk mode it filters keys of each record inside `states[entity_id]`; outer keys (`success`, `count`, `states`, `errors`, ...) are always preserved. `attribute_keys=` further narrows the `attributes` sub-dict and is only applied when `"attributes"` is in `fields=` (or `fields=None`); otherwise it is a no-op. When `attribute_keys=` is set but has no effect (because `attributes` was excluded by `fields=`), a `warnings` list is emitted outside the projected entity record(s): in bulk mode at the response wrapper level (sibling of `success`/`count`/`states`); in single-entity mode at the top-level result (sibling of `data`/`metadata`, since the projected record IS `data`). The warnings list is never a record key, so `fields=["state"]` returns a record with only `state` regardless of whether the no-effect warning fires. EXAMPLES: - Single: ha_get_state("light.kitchen") - Multiple: ha_get_state(["light.kitchen", "light.living_room", "sensor.temperature"]) - State only: ha_get_state("light.kitchen", fields=["state"]) - Slim bulk: ha_get_state(["light.kitchen", "sensor.temperature"], fields=["state", "attributes"], attribute_keys=["brightness"])
Parameters
entity_idrequired-(Annotated[str | list[str], JSON_STRING_COERCION, Field(description="Entity ID or list of entity IDs to retrieve state for (e.g., 'light.kitchen' or ['light.kitchen', 'sensor.temperature'])")])fields-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(default=None, description='Return only the specified top-level entity record keys to reduce response size (e.g. ["state", "attributes"]). None = full entity record (default). Available keys: entity_id, state, attributes, last_changed, last_reported, last_updated, context.')])= nullattribute_keys-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(default=None, description='Return only the specified keys from each entity\'s attributes dict (e.g. ["brightness", "color_temp_kelvin"] for lights). None = full attributes (default). Unknown keys are silently dropped. Requires "attributes" to be present in fields= (or fields=None).')])= nullDescription
Search for entities (lights, sensors, switches, climate, etc.) by name, domain, or area — AND inside automation/script/scene/helper/dashboard configurations — in one call. Two surfaces run in parallel and return tagged results: - **entities**: entity-registry matches (entity_id, friendly name, area). Filter with `domain_filter`/`area_filter`/`state_filter`; omit `query` to enumerate a domain, area, or state. - **automations / scripts / scenes / helpers / dashboards**: matches *inside* config definitions — triggers, actions, sequences, scene entity-sets, helper bodies, dashboard cards. Driven by `query`; narrow with `search_types`. Use this whenever you need to find something in HA without deciding entity-name vs config-body search up front. When NOT to use: - To read a known entity_id's state: use `ha_get_state` (cheaper). - To inspect one automation/script/scene config by id: use the matching `ha_config_get_*`. - To list installed add-ons: use `ha_get_addon`. Config-body search is skipped when `domain_filter`/`area_filter`/ `state_filter` signal entity-only intent (keeping name lookups off the expensive backend); a `warnings[]` entry names the skip. Pass `search_types=[...]` to force config search. Caveats: - `partial: True` means results are NOT exhaustive — a surface raised, or the config-body branch lost data (per-id time budget exhausted, an individual fetch failed, or a helper-type list fetch failed). Empty buckets with `partial: True` mean "search failed", not "no results". The cause is in `partial_reason`, also mirrored into `warnings[]` with an "incomplete results: " prefix. Do not treat a partial response as complete. - `count` is items in this response (post-pagination), not corpus totals — use `entity_total_matches` + `config_total_matches`. - `limit`/`offset` apply per-surface. Flat `has_more`/`next_offset` page the next call (iterate `offset = next_offset`); per-surface `entity_*`/`config_*` variants show which surface still has results. For parameters, schema, and worked examples, see ha_get_skill_guide. Examples: - List sensors in an area: ha_search(domain_filter="sensor", area_filter="Living Room") - Find a light by name: ha_search("kitchen", domain_filter="light") - Which automations use an entity: ha_search("light.bed_light") - Scenes touching a light: ha_search("light.kitchen", search_types=["scene"]) - Narrow the response to the entity bucket: ha_search("kitchen", fields=["entities"]) - All unavailable entities: ha_search(state_filter="unavailable")
Parameters
query-(Annotated[str | None, Field(default=None, description='What to search for (entity name fragment, free-text config term, entity_id). Searches BOTH the entity registry (entity_ids, friendly names, areas) AND configuration bodies (automation triggers/actions, script sequences, scene contents, helper bodies, dashboard cards) in one call. Use this for any find-something-in-HA question — entity OR config. Omit `query` to enumerate by `domain_filter`, `area_filter`, and/or `state_filter` alone (registry-listing mode); configuration-body search is skipped in that mode because there is no term to match against.')])= nulldomain_filter-(Annotated[str | None, Field(default=None, description="Narrow entity-registry results to a single domain (e.g. 'light', 'sensor'). Does not affect configuration search.")])= nullarea_filter-(Annotated[str | None, Field(default=None, description='Narrow entity-registry results to an area (id or name). Does not affect configuration search.')])= nullsearch_types-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(default=None, description="Configuration types to include in body search: 'automation', 'script', 'scene', 'helper', 'dashboard'. Default = automation+script+scene+helper. Pass as list or JSON-array string.")])= nulllimit-(Annotated[int, Field(default=10, ge=1, description='Maximum results per surface (entities, configs). Default: 10.')])= 10offset-(Annotated[int, Field(default=0, ge=0, description='Number of results to skip for pagination.')])= 0exact_match-(Annotated[bool, Field(default=True, description='Exact substring matching (default). Set False for fuzzy matching when the query may have typos.')])= trueinclude_hidden-(Annotated[bool, Field(default=True, description='Include hidden entities in registry results (with a score penalty so they sort below visible matches). Set False to exclude entirely.')])= trueinclude_config-(Annotated[bool, Field(default=False, description='Include full configuration bodies in body-search results. Default: False (summary only).')])= falsegroup_by_domain-(Annotated[bool, Field(default=False, description='Group entity-registry results by domain (entity-side only). Adds a `by_domain` map to the response.')])= falseper_domain_limit-(Annotated[int | None, Field(default=None, description='When `group_by_domain=True`, cap entity-registry results per domain to this number. Ignored otherwise.')])= nullstate_filter-(Annotated[str | None, Field(default=None, description='Filter entity-registry results to a specific state (e.g. "on", "off", "unavailable"). Case-insensitive. Can be used standalone (no query/domain/area) to enumerate every entity in that state; entity_total_matches reflects the filtered count.')])= nullresult_fields-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(default=None, description='Project each entity-registry record to only the specified keys (e.g. ["entity_id", "state"]). None = full records. Base keys: entity_id, friendly_name, domain, state, score, match_type. Opt-in enrichment keys (joined on request): area, floor, labels, aliases. An unknown key is rejected.')])= nullfields-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(default=None, description='Project the response to the named top-level keys (e.g. ["entities", "automations"]); None = full response. Diagnostic / pagination keys are always retained so projection cannot hide partial / error state. Distinct from `result_fields` (which projects each entity record\'s keys). Available keys: success, query, entities, automations, scripts, scenes, helpers, dashboards, search_types, search_type, entity_total_matches, config_total_matches, count, offset, limit, has_more, next_offset, entity_has_more, entity_next_offset, config_has_more, config_next_offset, by_domain, state_filter_note, area_names, domain_filter, area_filter, message, warnings, errors, partial, partial_reason.')])= nullconfig_time_budget-(Annotated[float | None, Field(default=None, gt=0, le=300, description="Per-call override for the per-id config-fetch wall-clock budget (seconds). Replaces the per-type HAMCP_*_CONFIG_TIME_BUDGET defaults for the automation, script, AND scene branches when their bulk-fetch falls through to per-id Attempt-C. Use when a `partial: True` response names time-budget skipping. Stateless per-call: one caller raising the budget doesn't affect others. None = use the per-type env defaults.")])= nullService & Device Control
Description
Control multiple devices with bulk operation support and WebSocket tracking.
Parameters
operationsrequired-(Annotated[list[dict[str, Any]], JSON_STRING_COERCION])parallel-(bool)= trueDescription
Execute a custom event on the Home Assistant event bus. When NOT to use: for controlling entities (lights, switches, climate) — use ha_call_service instead. For triggering automations by name, use ha_call_service("automation", "trigger"). Use this to publish custom event types consumed by event-triggered automations, Node-RED flows, or custom integrations that subscribe to specific event types. Caveats: Events are fire-and-forget; this tool confirms the event was accepted by the bus but does not verify whether any automation or subscriber acted on it.
Parameters
event_typerequired-(str)data-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION])= nullDescription
Execute Home Assistant services to control entities and trigger automations. This is the universal tool for controlling all Home Assistant entities. Services follow the pattern domain.service (e.g., light.turn_on, climate.set_temperature). **Basic Usage:** ```python # Turn on a light ha_call_service("light", "turn_on", entity_id="light.living_room") # Set temperature with parameters ha_call_service("climate", "set_temperature", entity_id="climate.thermostat", data={"temperature": 22}) # Trigger automation ha_call_service("automation", "trigger", entity_id="automation.morning_routine") # Universal controls work with any entity ha_call_service("homeassistant", "toggle", entity_id="switch.porch_light") ``` **Key behavior:** - **wait** (default True): wait for the entity state to change before returning. Only applies to state-changing services on a single entity. - **Result compaction (default ON)**: ``result`` is trimmed to the targeted entity's record (drops parent-group propagation) and stripped of ``context`` / ``last_*`` metadata and heavy attribute lists (``effect_list``, ``hue_scenes``). Escape hatches: ``verbose=True`` for the raw changed-state records, or ``result_fields`` / ``result_attribute_keys`` for explicit per-record projection (mirrors ``ha_get_state``). - **return_response** (default False): the service's response data is returned once, as the top-level ``service_response`` key — never nested inside ``result``, which carries the changed entity states. **For detailed service documentation, use ha_get_skill_guide.** Common patterns: Use ha_get_state() to check current values before making changes. Use ha_search() to find correct entity IDs. **WebSocket command escape hatch (advanced):** A few Home Assistant operations are WebSocket-only commands, not registered services — most notably dismissing a Repairs issue. Pass ``ws_command`` (instead of domain/service) to send one, with its parameters in ``data``: ```python # Dismiss a repair (get domain/issue_id from ha_get_overview repairs # or ha_get_system_health include="repairs") ha_call_service(ws_command="repairs/ignore_issue", data={"domain": "sun", "issue_id": "abc", "ignore": True}) ``` Only one-shot request/response commands are supported; streaming/two-phase and service-invoking commands are rejected, and the other service parameters (entity_id, return_response, etc.) don't apply.
Parameters
domain-(str | None)= nullservice-(str | None)= nullentity_id-(str | None)= nulldata-(Annotated[dict[str, Any] | None, JSON_STRING_COERCION])= nullreturn_response-(bool)= falsewait-(bool)= trueverbose-(Annotated[bool, Field(description="Return HA's raw changed-state records unchanged (default: False). Use as an escape hatch when you need the full propagation chain or raw attribute payload (debug / inspection). With return_response=True the response data still surfaces once as the top-level service_response key, never nested in result. WARNING: brings back token-bloat for nested-group targets — prefer result_fields / result_attribute_keys for targeted control.")])= falseresult_fields-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(default=None, description="Project each record in 'result' to only these top-level keys (e.g. ['entity_id', 'state']). Mirrors ha_get_state's fields=. Setting this DISABLES default compaction — no entity-id filter, no metadata strip — and applies the explicit projection instead.")])= nullresult_attribute_keys-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(default=None, description="Project each record's 'attributes' dict to only these keys (e.g. ['brightness', 'rgb_color']). Mirrors ha_get_state's attribute_keys=. Setting this DISABLES default compaction. Requires 'attributes' to be present in result_fields (or result_fields=None).")])= nullws_command-(Annotated[str | None, Field(default=None, description="Advanced escape hatch: send a raw one-shot Home Assistant WebSocket command that is NOT a registered service (e.g. 'repairs/ignore_issue' to dismiss a Repairs issue). When set, omit domain/service and the other service params; put the command's parameters in data. Streaming/two-phase and service-invoking commands (call_service, execute_script) are rejected.")])= nullDescription
Get the status of one or more device operations with real-time WebSocket verification. Pass a single operation_id string to check one operation, or a list of IDs to check multiple operations at once (bulk status). The timeout_seconds wait window bounds both modes. Bulk checks poll all operations concurrently under one shared window and report per-item failures inside detailed_results instead of aborting the batch. Use this to track operations initiated by ha_bulk_control or ha_call_service. For current entity states, use ha_get_state instead.
Parameters
operation_idrequired-(Annotated[str | list[str], JSON_STRING_COERCION, Field(description='Single operation ID or list of operation IDs to check. Use a single string for one operation, or a list for bulk status checks.')])timeout_seconds-(int)= 10Description
List available Home Assistant services with optional pagination and detail control. Discovers services/actions that can be called via ha_call_service. Use domain or query filters to narrow results. Defaults to summary mode (name + description only) to keep responses compact. Args: domain: Filter by domain (e.g., 'light', 'switch', 'climate'). query: Search in service names and descriptions. limit: Max services per page (default: 50). offset: Pagination offset (default: 0). detail_level: 'summary' (default) returns name/description only; 'full' includes parameter field schemas. Examples: # Browse first page of all services (compact) ha_list_services() # List all light services with full parameter details ha_list_services(domain="light", detail_level="full") # Search for temperature-related services ha_list_services(query="temperature") # Paginate through all services ha_list_services(offset=50)
Parameters
domain-(str | None)= nullquery-(str | None)= nulllimit-(Annotated[int, Field(default=50, ge=1, le=200, description='Max services to return per page (default: 50)')])= 50offset-(Annotated[int, Field(default=0, ge=0, description='Number of services to skip for pagination (default: 0)')])= 0detail_level-(Annotated[Literal['summary', 'full'], Field(default='summary', description="'summary': service name + description only (default). 'full': include parameter field schemas.")])= "summary"service_fields-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(default=None, description='Project each service record to only the specified keys. E.g. ["name", "description"] returns slim service records. None = full records (default). Unknown keys yield empty records. Available keys: name, description, domain, service, fields (full mode only), target (full mode only).')])= nullfields-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(default=None, description='Return only the specified top-level response keys to reduce response size (e.g. ["services"]). None = full response (default). Available keys: success, domains, services, total_count, count, offset, limit, has_more, next_offset, detail_level, filters_applied.')])= nullSystem
Description
Get the current YAML fragment under a key, from one config file or across a glob. Use before ha_config_set_yaml to inspect what a key currently holds, and to find which file defines it — the returned ``file`` + ``yaml_path`` are exactly the arguments that address the same fragment for an edit. Not for storage-mode items: automations, scripts and scenes created through the UI live outside YAML — read those with ha_config_get_automation, ha_config_get_script and ha_config_get_scene. This tool sees only what is written in the config files themselves. Reads the file, so it reflects what is on disk rather than what Home Assistant currently has loaded; a fragment edited but not yet reloaded differs from the running config. Comments and HA tags survive as written and a ``!secret`` is never resolved to its value, so ``content`` can be handed back to ha_config_set_yaml unchanged. Files outside the read allowlist stay unreadable, and secrets.yaml reads back with its values masked.
Parameters
yaml_pathrequired-(Annotated[str, Field(description="Dotted YAML key path to look up, e.g. 'alert2', 'mqtt', 'template'. Any key is readable — unlike ha_config_set_yaml, which only writes an allowlisted set.")])file-(Annotated[str, Field(default='configuration.yaml', description="Config-relative file to read. Accepts an fnmatch glob to search several files at once — 'packages/*.yaml' matches one directory level, not a nested tree. Use the glob to find which file defines a key.")])= "configuration.yaml"include_content-(Annotated[bool, Field(default=True, description='Return the round-trip YAML text of each match. Set False to discover only which files define the key.')])= trueinclude_parsed-(Annotated[bool, Field(default=False, description="Additionally return each match as structured data. HA tags stay in source form ('!secret api_key'), never resolved.")])= falseDescription
Update raw YAML configuration in configuration.yaml, packages/*.yaml, or themes/*.yaml (LAST RESORT). MUST call ha_get_skill_guide OR refer to your locally installed skills first. **WARNING:** Destructive, disabled by default. Dedicated tools exist for almost every use case and should be preferred: - Template sensors (state-based or trigger-based) -> ha_config_set_helper(helper_type='template') - Automations (storage-mode) -> ha_config_set_automation - Scripts (storage-mode) -> ha_config_set_script - Scenes (storage-mode) -> ha_config_set_scene - All 28 helper types (input_*, counter, timer, schedule, zone, person, tag, group, min_max, threshold, derivative, statistics, utility_meter, trend, filter, switch_as_x, etc.) -> ha_config_set_helper Intended for YAML-only integrations with no config-flow or API equivalent (command_line, rest, shell_command, notify platforms), for integrations with significant YAML-only configuration (knx entities in package files), for registering YAML-mode dashboards via ``lovelace.dashboards.<url_path>`` (no other ``lovelace.*`` keys), and for editing theme files in ``themes/*.yaml`` (keyed by theme name; ``frontend.reload_themes`` is triggered automatically so no restart is needed). Themes only load when configuration.yaml carries the ``frontend: themes:`` include (e.g. ``!include_dir_merge_named themes``); this tool cannot add that include (``frontend`` is not an allowed key). Also accepts ``automation``, ``script``, and ``scene`` keys when ``file`` is a ``packages/*.yaml`` — for git-managed YAML configs that track these alongside templates and other YAML items. Writes to ``configuration.yaml`` for those three keys remain rejected so storage-mode and YAML-mode collections don't collide; use the dedicated storage-mode tools instead. Check ``post_action`` in the response: most keys need a full HA restart. For ``themes/*.yaml`` this tool *performs* the reload itself (``frontend.reload_themes``), so ``post_action`` is ``reload_performed`` (or ``reload_available`` plus ``reload_error`` if that reload failed). For template, mqtt, group, automation, script, and scene it only *advertises* the reload service (``post_action: reload_available`` with a ``reload_service`` to call yourself); the edit is on disk but not yet live. Preserves YAML comments and HA tags (``!include``, ``!secret``) on round-trip; ``replace`` swaps the subtree as-is. Two-step confirmation (default ON, toggle ENABLE_YAML_EDIT_CONFIRM / Server Settings): the first call returns ``preview: true`` with a unified ``diff`` of exactly what would change on disk and a ``confirm_token`` — nothing is written. Review the diff for changes outside the requested edit, then repeat the identical call adding ``confirm_token`` to apply. Every applied write also returns the final ``diff``. A token mismatch means the file changed since the preview (or the token was wrong); use the freshly returned token. ``template-guidelines.md`` ships in this response under ``skill_content`` by default — YAML packages frequently include template sensors / command_line entities / mqtt templates, exactly where template misuse causes the subtlest bugs. For deeper routing guidance beyond what ships here, use ha_get_skill_guide.
Parameters
yaml_pathrequired-(Annotated[str, Field(description="Top-level YAML key to modify. Only a narrow allowlist of YAML-only or YAML-heavy integration keys is accepted (e.g., 'command_line', 'rest', 'shell_command', 'notify', 'knx'). For YAML-mode dashboards, use the dotted form 'lovelace.dashboards.<url_path>' where <url_path> is lowercase, hyphenated, and not a reserved HA route. For themes in themes/*.yaml, use the theme name (simple name without dots; content is the mapping of theme variables only, without the theme name). 'automation', 'script', and 'scene' are accepted only when file is under packages/*.yaml; in configuration.yaml use the dedicated storage-mode tools (ha_config_set_automation, ha_config_set_script, ha_config_set_scene). Not for template sensors or input_* helpers — those have dedicated tools.")])actionrequired-(Annotated[str, Field(description="Action to perform: 'add' (insert/merge content under key), 'replace' (overwrite key with new content), or 'remove' (delete the key entirely).")])content-(Annotated[str | None, Field(default=None, description="YAML content for the value under yaml_path. Required for 'add' and 'replace' actions. Must be valid YAML.")])= nullfile-(Annotated[str, Field(default='configuration.yaml', description="Relative path to the YAML config file. Defaults to 'configuration.yaml'. Also supports 'packages/*.yaml' and 'themes/*.yaml' (yaml_path is the theme name; frontend.reload_themes is triggered automatically).")])= "configuration.yaml"confirm_token-(Annotated[str | None, Field(default=None, description='Confirmation token from a prior preview response. When the YAML edit confirmation flow is enabled (default), the first call writes nothing and returns a unified diff plus confirm_token; repeat the identical call with that token to apply the edit.')])= nullMandatoryBPS-(Annotated[bool, Field(default=True)])= trueBestPracticeKey-(BestPracticeKeyParam)= nullDescription
Polymorphic backup tool. See the tool description for the routing matrix.
Parameters
scoperequired-(Annotated[Literal['snapshot', 'edits'], Field(description="'snapshot' for full HA tarballs; 'edits' for per-entity auto-backups.")])actionrequired-(Annotated[Literal['create', 'restore', 'list', 'view', 'diff', 'delete'], Field(description='Operation to perform. Valid (scope, action) combinations are listed in the tool description.')])name-(Annotated[str | None, Field(default=None, description='(snapshot.create) Tarball name. Auto-generated if not provided.')])= nullbackup_id-(Annotated[str | None, Field(default=None, description="(snapshot.restore / snapshot.delete) Tarball ID (e.g. 'dd7550ed').")])= nullrestore_database-(Annotated[bool, Field(default=False, description='(snapshot.restore) Include database in the restore. Default false (config-only).')])= falseconfirm-(Annotated[bool, Field(default=False, description='(snapshot.delete) Must be True to confirm deletion — a safety measure against accidental calls.')])= falsedomain-(Annotated[str | None, Field(default=None, description="(edits.list / edits.delete) Filter auto-backups by domain (e.g. 'automation', 'helper_timer').")])= nullentity_id-(Annotated[str | None, Field(default=None, description='(edits.list / edits.delete) Filter auto-backups by entity ID.')])= nullbackup_name-(Annotated[str | None, Field(default=None, description="(edits.view / edits.restore / edits.delete) Auto-backup filename (format '<domain>.<entity_id>.<timestamp>.yaml'). Not a tarball ID.")])= nullolder_than_days-(Annotated[int | None, Field(default=None, ge=0, description='(edits.delete) Bulk-delete auto-backups older than this many days.')])= nulllimit-(Annotated[int, Field(default=200, ge=1, le=10000, description='(edits.list / snapshot.list) Maximum number of entries to return.')])= 200Description
Create and run a custom tool in a sandbox, or manage saved custom tools. ⚠️ **LAST RESORT** — search for existing tools first. **Modes** (mutually exclusive): - Provide ``code`` + ``justification`` to execute custom code - Set ``run_saved`` to re-run a previously saved tool by name - Set ``list_saved=True`` to list all saved tools **Available functions in sandbox:** - ``api_get(endpoint)`` — GET request to HA REST API - ``api_post(endpoint, data)`` — POST request to HA REST API - ``ws_send(message)`` — send a HA WebSocket command (e.g. registry lookups, ``render_template``, dashboard ops). ``message`` must include a ``"type"`` field; the MCP server adds ``id`` and handles auth. - ``call_tool(name, args)`` — call a registered MCP tool - ``delete_saved_tool(name)`` — remove a previously saved custom tool by name. Returns ``{"deleted": True, "name": name}`` or ``{"error": ...}``. Use ``api_get``/``api_post`` for REST operations not covered by existing tools. Use ``ws_send`` when the operation is only available over the Home Assistant WebSocket API (most registry CRUD, template rendering, and Lovelace operations). Use ``call_tool`` when an existing tool already does what you need. Use ``delete_saved_tool`` to clean up saved tools you no longer need. Saved tools persist across server restarts when ``CODE_MODE_SAVED_TOOLS_PATH`` is set (the addon sets this by default to ``/data/saved_tools.json``). Example — check repairs (no built-in tool for this): ```python repairs = await api_get("/repairs/issues") repairs ``` Example — list areas via WebSocket: ```python result = await ws_send({"type": "config/area_registry/list"}) result.get("result", []) ``` Example — chain existing tools: ```python result = await call_tool("ha_search", {"query": "light", "limit": 5}) data = result.get("data", result) lights = data.get("results", []) for e in lights: await call_tool("ha_call_service", { "domain": "light", "service": "turn_off", "entity_id": e["entity_id"]}) {"turned_off": len(lights)} ``` Example — delete an obsolete saved tool: ```python delete_saved_tool("old_movie_mode") ``` Args: code: Python code to execute. Last expression is the return value. justification: Why no existing tool works (required with code). save_as: Save the tool under this name for reuse (alphanumeric/underscores, max 64 chars). run_saved: Name of a previously saved tool to re-run. list_saved: Set True to list all saved tools.
Parameters
code-(str | None)= nulljustification-(str | None)= nullsave_as-(str | None)= nullrun_saved-(str | None)= nulllist_saved-(bool)= falseDescription
Manage Home Assistant frontend themes. When NOT to use: themes are YAML files - Home Assistant has no API to create or edit them. Installing community themes goes through HACS (ha_manage_hacs); editing custom theme files goes through ha_config_set_yaml (beta, edits themes/<name>.yaml keyed by theme name and attempts an automatic theme reload). When to use: action='list' discovers installed theme names and the current defaults; action='set' selects the backend default theme (optionally per light/dark mode). Caveats: action='set' changes the backend-selected default only - users who explicitly picked a theme in their profile keep their choice. Theme names are validated by Home Assistant at call time. EXAMPLES: - List themes: ha_manage_theme(action="list") - Set default theme: ha_manage_theme(action="set", theme_name="nord") - Set dark-mode theme: ha_manage_theme( action="set", theme_name="nord", mode="dark") - Restore built-in default: ha_manage_theme( action="set", theme_name="default")
Parameters
actionrequired-(Annotated[ThemeAction, Field(description='Theme operation: list installed themes or set the default theme.')])theme_name-(Annotated[str | None, Field(description="Theme name when action='set'. Must be an installed theme; 'default' restores the built-in theme, 'none' resets the chosen mode to the built-in default.", default=None)])= nullmode-(Annotated[Literal['light', 'dark'] | None, Field(description="Which mode the theme applies to when action='set'. Defaults to light.", default=None)])= nullDescription
Manage Home Assistant updates -- list, read details, batch install, skip, or un-skip. Covers Core, OS, supervisor, apps (add-ons), device firmware, and HACS update entities. In Read Only Mode the read actions ('list', 'get') stay available; write actions are blocked. Installs run asynchronously in Home Assistant and can take minutes: 'install' returns once the service calls are accepted, with per-entity results. Poll action='list' to watch in_progress until installed_version reaches latest_version. EXAMPLES: - List all updates: ha_manage_updates() - Pre-update analysis: ha_manage_updates(action="get", entity_ids=["update.home_assistant_core_update"], include_release_notes=True) - Update everything pending in a category: ha_manage_updates(action="install", categories=["addons", "hacs"]) RETURNS (action='list'): updates_available, updates, categories, and ha_mcp_update -- this MCP server's own update status {current, latest, update_available}, so a newer ha-mcp release can be flagged. RETURNS (action='get'): update details, release notes; with include_release_notes=True on Core also breaking_changes.entries[], multi_version_release_notes[], and installed_integrations.
Parameters
action-(Annotated[str, Field(description="'list' (all pending updates, default), 'get' (details/release notes for one update), 'install' (apply pending updates), 'skip' (hide the offered version), or 'clear_skipped' (re-offer a skipped version).", default='list')])= "list"entity_ids-(Annotated[list[str] | str | None, JSON_STRING_COERCION, Field(description="Update entity_id(s) to act on. 'get' takes exactly one; skip/clear_skipped require at least one; for install, mutually exclusive with categories.", default=None)])= nullcategories-(Annotated[list[str] | str | None, JSON_STRING_COERCION, Field(description="For install: apply every pending update in these categories ('addons', 'hacs', 'devices', 'other'). Mirrors the HA 2026.7 'Update all' button: core/os/supervisor are excluded by design (target those individually via entity_ids) and skipped updates are never included.", default=None)])= nullinclude_skipped-(Annotated[bool, Field(description='For list: include updates that have been skipped (default: False).', default=False)])= falseinclude_release_notes-(Annotated[bool, Field(description='For get on a Core update entity: fetch multi-version release notes and breaking changes for all versions between installed and latest (default: False). Adds breaking_changes, multi_version_release_notes, and installed_integrations to the response.', default=False)])= falsebackup-(Annotated[bool, Field(description='For install: create a backup before installing where the update entity supports it (apps/add-ons). Default: False.', default=False)])= falseDescription
Reload Home Assistant configuration without full restart. This tool reloads specific configuration components, allowing changes to take effect without restarting the entire Home Assistant instance. This is much faster than a full restart. **Parameters:** - target: What to reload. Options: - "all": Reload all reloadable components - "automations": Reload automation configurations - "scripts": Reload script configurations - "scenes": Reload scene configurations - "groups": Reload group configurations - "input_booleans": Reload input_boolean helpers - "input_numbers": Reload input_number helpers - "input_texts": Reload input_text helpers - "input_selects": Reload input_select helpers - "input_datetimes": Reload input_datetime helpers - "input_buttons": Reload input_button helpers - "timers": Reload timer helpers - "templates": Reload template sensors/entities - "persons": Reload person configurations - "zones": Reload zone configurations - "core": Reload core configuration (customize, packages) - "themes": Reload frontend themes - entry_id: Reload a SINGLE config entry (one integration instance) instead of sweeping subsystems — the fast path after editing a custom component on disk. Pass it alone (leave `target` at its "all" default); combining it with an explicit `target` is a validation error. Find the id via ha_get_integration. **Example Usage:** ```python # Reload just automations after editing ha_reload_core(target="automations") # Reload all configurations ha_reload_core(target="all") # Reload input helpers after adding new ones ha_reload_core(target="input_booleans") ``` **When to Use:** - After editing automation/script YAML files - After adding new input helpers via YAML - After modifying customize.yaml - After theme changes
Parameters
target-(str)= "all"entry_id-(str | None)= nullDescription
Restart Home Assistant. **WARNING: This will restart the entire Home Assistant instance!** All automations will be temporarily unavailable during restart. The restart typically takes 1-5 minutes depending on your setup. **Parameters:** - confirm: Must be set to True to confirm the restart. This is a safety measure to prevent accidental restarts. **Best Practices:** 1. Config is validated automatically before the restart proceeds; to pre-check, call ha_get_system_health(include="config_check") 2. Notify users before restarting (if applicable) 3. Schedule restarts during low-activity periods **Example Usage:** ```python # Optional pre-check (ha_restart also validates config automatically) health = ha_get_system_health(include="config_check") if health["config_check"]["is_valid"]: # Restart with confirmation result = ha_restart(confirm=True) ``` **Alternative:** For configuration changes, consider using ha_reload_core() instead, which reloads specific components without a full restart.
Parameters
confirm-(bool)= falseTodo Lists
Description
Get todo lists or items - list all todo lists or get items from a specific list. Without an entity_id: Lists all todo list entities in Home Assistant. With an entity_id: Gets items from that specific todo list, optionally filtered by status. **LISTING TODO LISTS (entity_id omitted):** Returns all entities in the 'todo' domain, including shopping lists and any other todo-type integrations. Each todo list includes: - entity_id: The unique identifier (e.g., 'todo.shopping_list') - friendly_name: Human-readable name - state: Number of incomplete items or current status **GETTING TODO ITEMS (entity_id provided):** Retrieves items from the specified todo list. Status filter values: - needs_action: Items that still need to be done - completed: Items that have been marked as done - None (default): Returns all items regardless of status Item properties: - uid: Unique identifier for the item - summary: The item text/description - status: Current status (needs_action or completed) - description: Optional detailed description - due: Optional due date (if supported) EXAMPLES: - List all todo lists: ha_get_todo() - Get all items: ha_get_todo("todo.shopping_list") - Get incomplete items: ha_get_todo("todo.shopping_list", status="needs_action") - Get completed items: ha_get_todo("todo.shopping_list", status="completed") USE CASES: - "What todo lists do I have?" - "Show me my shopping list" - "What's on my todo list?" - "Show completed items"
Parameters
entity_id-(Annotated[str | None, Field(description="Todo list entity ID (e.g., 'todo.shopping_list'). If omitted, lists all todo list entities.", default=None)])= nullstatus-(Annotated[Literal['needs_action', 'completed'] | None, Field(description="Filter items by status: 'needs_action' for incomplete, 'completed' for done. Only applies when entity_id is provided.", default=None)])= nullDescription
Remove an item from a Home Assistant todo list. Permanently deletes an item from the specified todo list. IDENTIFYING ITEMS: - Use the item's UID (from ha_get_todo) - Or use the exact item summary/name text EXAMPLES: - Remove by name: ha_remove_todo_item("todo.shopping_list", "Buy milk") - Remove by UID: ha_remove_todo_item("todo.shopping_list", "abc123-uid") USE CASES: - "Remove milk from my shopping list" - "Delete the eggs item" - "Clear 'call mom' from my todo" WARNING: This permanently removes the item. To mark as completed instead, use ha_set_todo_item() with status="completed".
Parameters
entity_idrequired-(Annotated[str, Field(description="Todo list entity ID (e.g., 'todo.shopping_list')")])itemrequired-(Annotated[str, Field(description='Item to remove - can be the item UID or the exact item summary/name')])Description
Create or update a todo item in Home Assistant. WITHOUT item parameter (create mode): Creates a new item. summary is required. WITH item parameter (update mode): Updates an existing item identified by UID or exact name. At least one update field (rename, status, description, due_date, due_datetime) is required. EXAMPLES: - Add item: ha_set_todo_item("todo.shopping_list", summary="Buy milk") - Add with description: ha_set_todo_item("todo.shopping_list", summary="Buy milk", description="2% organic") - Add with due date: ha_set_todo_item("todo.tasks", summary="Pay bills", due_date="2024-12-31") - Complete item: ha_set_todo_item("todo.shopping_list", item="Buy milk", status="completed") - Rename item: ha_set_todo_item("todo.tasks", item="Old task", rename="New task name") - Update due date: ha_set_todo_item("todo.tasks", item="Pay bills", due_date="2024-12-31") - Reopen item: ha_set_todo_item("todo.tasks", item="Task to redo", status="needs_action") NOTE: Not all todo integrations support all features (description, due dates). The Shopping List integration only supports summary.
Parameters
entity_idrequired-(Annotated[str, Field(description="Todo list entity ID (e.g., 'todo.shopping_list')")])summary-(Annotated[str | None, Field(description="Item text/name. Required when creating a new item. Ignored in update mode — use 'rename' to change the item name.", default=None)])= nullitem-(Annotated[str | None, Field(description='Existing item to update - can be the item UID or the exact item summary/name. When provided, operates in update mode. When omitted, creates a new item.', default=None)])= nullstatus-(Annotated[Literal['needs_action', 'completed'] | None, Field(description="Item status: 'completed' to mark done, 'needs_action' to mark incomplete. Only used in update mode.", default=None)])= nulldescription-(Annotated[str | None, Field(description='Detailed description for the item', default=None)])= nulldue_date-(Annotated[str | None, Field(description="Due date in YYYY-MM-DD format (e.g., '2024-12-25')", default=None)])= nulldue_datetime-(Annotated[str | None, Field(description="Due datetime in ISO format (e.g., '2024-12-25T14:00:00'). Overrides due_date if both provided.", default=None)])= nullrename-(Annotated[str | None, Field(description='New name/summary for an existing item. Only used in update mode.', default=None)])= nullUtilities
Description
Evaluate Jinja2 templates using Home Assistant's template engine. This tool allows testing and debugging of Jinja2 template expressions that are commonly used in Home Assistant automations, scripts, and configurations. It provides real-time evaluation with access to all Home Assistant states, functions, and template variables. **When NOT to use this for automation/script logic:** Templates have legitimate uses (notification bodies, dynamic `data.*` values, debugging existing templates), but `condition:` / `trigger:` positions and action service names are better expressed as native HA constructs: native constructs are schema-validated at config load and surface structural errors loudly, whereas equivalent template logic only errors at runtime — and a template that renders a non-truthy value is silently treated as false. Prefer: - `condition: numeric_state` over `{{ states('x') | float > N }}` - `condition: state` over `{{ is_state(...) }}` - `condition: time` / `condition: sun` over `now().hour` / `is_state('sun.sun', ...)` - Native `for:` field on state/numeric_state triggers and state conditions over `{{ now() - X.last_changed > timedelta(...) }}` duration math - `choose` action over templated `service:` / `action:` strings See `ha_get_skill_guide` (best-practices skill) for the full anti-pattern list. **When to use (reach for this tool, don't compute it yourself):** Any one-shot question whose answer is DERIVED from current HA state — an average/sum/min/max across sensors, a count of entities matching a condition, a boolean comparison, or a rendered message with live values. One render call beats fetching N states and doing the math yourself, and it is the canonical way to *test* a template before embedding it. This is for one-shot answers and template testing only — NOT for putting templates into automation logic; for `condition:` / `trigger:` positions native constructs win. - "average temperature across the bedroom sensors" -> `{{ ([states('sensor.a'), states('sensor.b')] | map('float', 0) | sum) / 2 }}` - "how many lights are on" -> `{{ states.light | selectattr('state', 'eq', 'on') | list | count }}` NOT for a plain single-entity value ("what's the state of X") — that is `ha_get_state` / `ha_search`; rendering `{{ states('X') }}` there is over-use. **Parameters:** - template: The Jinja2 template string to evaluate - timeout: Maximum evaluation time in seconds (default: 3) - report_errors: Whether to return detailed error information (default: True) **Common Template Functions:** **State Access:** ```jinja2 {{ states('sensor.temperature') }} # Get entity state value {{ states.sensor.temperature.state }} # Alternative syntax {{ state_attr('light.bedroom', 'brightness') }} # Get entity attribute {{ is_state('light.living_room', 'on') }} # Check if entity has specific state ``` **Numeric Operations:** ```jinja2 {{ states('sensor.temperature') | float(0) }} # Convert to float with default {{ states('sensor.humidity') | int(0) }} # Convert to integer with default {{ (states('sensor.temp') | float(0) + 5) | round(1) }} # Math operations ``` **Time and Date:** ```jinja2 {{ now() }} # Current datetime {{ now().strftime('%H:%M:%S') }} # Format current time {{ as_timestamp(now()) }} # Convert to Unix timestamp {{ now().hour }} # Current hour (0-23) {{ now().weekday() }} # Day of week (0=Monday) ``` **Conditional Logic (for display strings — not for `condition:` positions):** ```jinja2 {{ 'Day' if now().hour < 18 else 'Night' }} # Ternary operator {% if is_state('alarm_control_panel.home', 'armed_away') %} Alarm is armed {% else %} Alarm is disarmed {% endif %} ``` **Lists and Loops:** ```jinja2 {% for entity in states.light %} {{ entity.entity_id }}: {{ entity.state }} {% endfor %} {{ states.light | selectattr('state', 'eq', 'on') | list | count }} # Count on lights ``` **String Operations:** ```jinja2 {{ states('sensor.weather') | title }} # Title case {{ 'Hello ' + states('input_text.name') }} # String concatenation {{ states('sensor.data') | regex_replace('pattern', 'replacement') }} ``` **Device and Area Functions:** ```jinja2 {{ device_entities('device_id_here') }} # Get entities for device {{ area_entities('living_room') }} # Get entities in area {{ device_id('light.bedroom') }} # Get device ID for entity ``` **Common Use Cases (legitimate template positions):** **Dynamic Service Data:** ```jinja2 # Dynamic brightness based on time {{ 255 if now().hour < 22 else 50 }} # Message with current values "Temperature is {{ states('sensor.temp') }}°C, humidity {{ states('sensor.humidity') }}%" ``` **Examples:** **Test basic state access:** ```python ha_eval_template("{{ states('light.living_room') }}") ``` **Test a string expression (e.g. for a notification body):** ```python ha_eval_template("{{ 'Day' if now().hour < 18 else 'Night' }}") ``` **Test mathematical operations:** ```python ha_eval_template("{{ (states('sensor.temperature') | float(0) + 5) | round(1) }}") ``` **Test entity counting:** ```python ha_eval_template("{{ states.light | selectattr('state', 'eq', 'on') | list | count }}") ``` **IMPORTANT NOTES:** - Templates have access to all current Home Assistant states and attributes - Use this tool to test templates before using them in automations or scripts - Template evaluation respects Home Assistant's security model and timeouts - Complex templates may affect Home Assistant performance - keep them efficient - Use default values (e.g., `| float(0)`) to handle missing or invalid states **For template documentation:** https://www.home-assistant.io/docs/configuration/templating/
Parameters
templaterequired-(str)timeout-(int)= 3report_errors-(bool)= trueDescription
Get diagnostic information and templates for filing issue reports or feedback. This tool generates templates for TWO types of reports: 1. **Runtime Bug Report** - For ha-mcp errors, failures, unexpected behavior 2. **Agent Behavior Feedback** - For AI agent inefficiency, wrong tool usage **IMPORTANT FOR AI AGENTS:** You MUST analyze the conversation context to determine which template to present: 🐛 **Present RUNTIME BUG template if:** - User reports an error, failure, or unexpected behavior - A tool returned an error or incorrect result - Something is broken or not working in ha-mcp 🤖 **Present AGENT BEHAVIOR template if:** - User mentions YOU (the agent) used the wrong tool - User suggests a more efficient workflow - User reports YOUR inefficiency or mistakes - User says you should have done something differently **If unclear which type, ASK the user:** "Are you reporting a bug in ha-mcp, or providing feedback on how I used the tools?" **WHEN TO USE THIS TOOL:** - "I want to file a bug/issue/report" - "This isn't working" - "You should have used [other tool]" - "That was inefficient" **OUTPUT:** Returns both templates plus diagnostic data. The full response is LARGE (the captured logs appear in the raw log keys AND inside each template) — pass fields=... to fetch only the keys you need once you know which template applies. Key fields: - `runtime_bug_template`, `agent_behavior_template` — pick based on context - `recent_logs`, `startup_logs` — captured ha-mcp tool/server log entries - `addon_logs` — addon container stdout/stderr (HA add-on installs only; empty string otherwise) - `core_error_log` — Home Assistant error log (home-assistant.log) over REST; carries auth / integration errors that don't show in addon_logs - `missing_tool_hint` — check this FIRST when the report is about a missing/unavailable tool; a stale client tool list (not a bug) is the usual cause, and refreshing the MCP connection is the fix - `suggested_title`, `duplicate_check_urls`, `anonymization_guide`
Parameters
tool_call_count-(Annotated[int, Field(default=10, ge=1, le=16, description='Number of tool calls made since the issue started. This determines how many log entries to include. Count how many ha_* tools were called from when the issue began. Default: 10. Max: 16 (limited by 200-entry log buffer: 16*4*3=192)')])= 10fields-(Annotated[str | list[str] | None, JSON_STRING_COERCION, Field(default=None, description="Return only the specified top-level response keys — the full response (both templates + logs + diagnostics, with log content repeated across the raw keys and templates) is very large. None = full response. Typical for a runtime bug: 'runtime_bug_template,suggested_title,runtime_bug_submit_url,duplicate_check_urls,anonymization_guide,instructions'; for agent feedback swap in agent_behavior_template and agent_behavior_submit_url. The templates already embed the relevant logs, so the raw log keys are only needed for your own analysis. Available keys: diagnostic_info, recent_logs, startup_logs, addon_logs, core_error_log, log_count, startup_log_count, formatted_report, runtime_bug_template, agent_behavior_template, anonymization_guide, suggested_title, runtime_bug_submit_url, agent_behavior_submit_url, duplicate_check_urls, missing_tool_hint, instructions.")])= nullZones
Description
Get zone information - list all zones or get details for a specific one. Without a zone_id: Lists all Home Assistant zones with their coordinates and radius. With a zone_id: Returns detailed configuration for a specific zone. ZONE PROPERTIES: - ID, name, icon - Latitude, longitude, radius - Passive mode setting EXAMPLES: - List all zones: ha_get_zone() - Get specific zone: ha_get_zone(zone_id="abc123") **NOTE:** With the ha_mcp_tools custom component installed, YAML-defined zones — including the auto-synthesized 'home' zone — are included and marked ``editable=false`` / ``source="yaml"`` (storage zones created via UI/API are ``source="storage"``). Without the component, only storage zones are listed and YAML-defined zones such as 'home' will not appear.
Parameters
zone_id-(Annotated[str | None, Field(description='Zone ID to get details for (from ha_get_zone() list). If omitted, lists all zones.', default=None)])= nullDescription
Remove a Home Assistant zone. EXAMPLES: - Remove zone: ha_remove_zone("abc123") **WARNING:** Removing a zone used in automations may cause those automations to fail. Use ha_get_zone() to find the zone_id for the zone you want to remove. **NOTE:** The 'home' zone cannot be removed as it is typically defined in configuration.yaml.
Parameters
zone_idrequired-(Annotated[str, Field(description='Zone ID to remove (use ha_get_zone to find IDs)')])Description
Create or update a Home Assistant zone. Omit zone_id to create a new zone (name, latitude, longitude required). Provide zone_id to update an existing zone (only specified fields change). EXAMPLES: - Create: ha_set_zone(name="Office", latitude=40.7128, longitude=-74.0060, radius=150, icon="mdi:briefcase") - Update name: ha_set_zone(zone_id="abc123", name="New Office") - Update radius: ha_set_zone(zone_id="abc123", radius=200) - Update location: ha_set_zone(zone_id="abc123", latitude=40.7128, longitude=-74.0060) Note: The 'home' zone is typically defined in YAML and cannot be modified via this API.
Parameters
name-(Annotated[str | None, Field(description='Display name for the zone (required for create)', default=None)])= nulllatitude-(Annotated[float | None, Field(description='Latitude coordinate of the zone center (required for create)', default=None)])= nulllongitude-(Annotated[float | None, Field(description='Longitude coordinate of the zone center (required for create)', default=None)])= nullzone_id-(Annotated[str | None, Field(description='Zone ID to update (omit to create new zone, use ha_get_zone to find IDs)', default=None)])= nullradius-(Annotated[float | None, Field(description='Radius of the zone in meters (must be > 0, defaults to 100 on create)', default=None)])= nullicon-(Annotated[str | None, Field(description="Material Design Icon (e.g., 'mdi:briefcase', 'mdi:school')", default=None)])= nullpassive-(Annotated[bool | None, Field(description='Passive mode - if True, zone will not trigger enter/exit automations (defaults to False on create)', default=None)])= null