Skip to main content
Glama
homeassistant-ai

Home Assistant MCP Server

Official

Create or Update Dashboard

ha_config_set_dashboard
Destructive

Create or update Home Assistant dashboards by replacing full config, applying structured JSON patches, or running Python transforms on existing views and cards.

Instructions

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 full config replacement, Python transformation, or structured patch edits.

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:

  • patch: Edit known paths with literal values using add/remove/replace/test and config_hash. Example: patch=[{"op": "replace", "path": "/views/0/title", "value": "Home"}]. Append with /views/0/cards/-; escape ~ as ~0 and / as ~1 in path keys. move/copy are unsupported. See the full patch guide: https://github.com/homeassistant-ai/ha-mcp/blob/master/docs/dashboard-edits.md

  • python_transform: Use loops or pattern-based changes across cards and views.

  • 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.

TIP: return_screenshot=True bundles rendered image(s) with the write result (beta feature); for visual re-checks after the write, use the dedicated ha_get_dashboard_screenshot tool instead of re-sending config.

PYTHON TRANSFORM EXAMPLES:

  • 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: title/icon/require_admin/show_in_sidebar can be updated in metadata-only calls or alongside a full config replacement. For python_transform or patch, update metadata in a separate call; combining it with patch is rejected.

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.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
iconNoMDI icon name (e.g., 'mdi:home', 'mdi:cellphone'). Defaults to 'mdi:view-dashboard'
patchNoStructured dashboard edits: up to 100 JSON Patch add, remove, replace or test operations using RFC 6901 paths. Use /- to append to an array; escape ~ as ~0 and / as ~1 in keys. Requires config_hash. Mutually exclusive with config and python_transform. Update title/icon/require_admin/show_in_sidebar in a separate call. Strings in value are preserved literally.
titleNoDashboard display name shown in sidebar
configNoDashboard configuration with views and cards. Omit or set to None to create dashboard without initial config. Mutually exclusive with python_transform and patch.
url_pathYesDashboard URL path (e.g., 'my-dashboard'). Use 'default' or 'lovelace' for the default dashboard. New dashboards must use a hyphenated path.
view_pathNoWith return_screenshot: stable Lovelace views[].path to render.
config_hashNoConfig hash from ha_config_get_dashboard for optimistic locking. REQUIRED for python_transform and patch (validates dashboard unchanged). Optional for config (validates before full replacement if provided).
MandatoryBPSNo
require_adminNoRestrict dashboard to admin users only. For existing dashboards, only updated when explicitly provided.
BestPracticeKeyNoRead-receipt for the home-assistant-best-practices skill; required when strict best-practices mode is enabled. Not a secret or credential: the current value is an attestation phrase published openly at the top of the skill content served by ha_get_skill_guide. Read that content, then pass the value back verbatim — this round-trip is the server's designed protocol confirming the practices were read before writing.
show_in_sidebarNoShow dashboard in sidebar navigation. For existing dashboards, only updated when explicitly provided.
python_transformNoPython expression to transform existing dashboard config. Mutually exclusive with config and patch. 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]" PYTHON TRANSFORM SECURITY: ✅ ALLOWED: - Dictionary/list access: config['views'][0]['cards'][1] - Slicing: config['views'][0]['cards'][1:3] - Assignment: config['key'] = 'value' - Deletion: del config['key'] or config.pop('key') - List methods: append, insert, pop, remove, clear, extend - Dict methods: update, get, setdefault, keys, values, items - Loops: for, if/else, pass, break, continue - Comprehensions: [x for x in ...], {k: v for ...}, (x for x in ...) - Ternary: x if condition else y - Iterable unpacking (* in calls/literals): f(*xs), [*xs, y] - Dict unpacking (**) in calls and dict literals: {**d, 'k': v} - Keyword arguments: func(key=value) - Lambdas (e.g. for `key=`): sorted(items, key=lambda x: x['score']) - String methods: startswith, endswith, lower, upper, strip, split, join, replace - Safe builtins: isinstance, len, range, enumerate, zip, sorted, reversed, min, max, sum, abs, any, all, round, str, int, float, bool, list, dict, tuple, set ❌ FORBIDDEN: - Imports: import, from, __import__ - File operations: open, read, write - Dunder access: __class__, __bases__, __subclasses__ - Dangerous builtins: eval, exec, compile, getattr, setattr, delattr, hasattr - Function definitions: def, class - Exception handling: try/except (validate with isinstance/in/.get() instead) - While loops: use bounded for loops or comprehensions instead 🎯 PATTERNS: - Filter cards: cards = [c for c in cards if keep(c)] - Skip in a loop: prefer `continue` over an empty `pass` branch (clearer) - Conditionally include: build a new list and `.append(x)` only the cards you want, instead of iterating the original and using if/pass branches to drop entries - Modify in place when possible (single pass, fewer surprises) over reconstructing the entire list
return_screenshotNoAfter 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. For visual re-checks after the write (no config round-trip), use the dedicated ha_get_dashboard_screenshot tool instead.

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed4 schema fields changedv8.5.0
    • changedInput schema / properties / config / description
      Previous value: -"Dashboard configuration with views and cards. Omit or set to None to create dashboard without initial config. Mutually exclusive with python_transform."New value: +"Dashboard configuration with views and cards. Omit or set to None to create dashboard without initial config. Mutually exclusive with python_transform and patch."
    • changedInput schema / properties / config_hash / description
      Previous value: -"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)."New value: +"Config hash from ha_config_get_dashboard for optimistic locking. REQUIRED for python_transform and patch (validates dashboard unchanged). Optional for config (validates before full replacement if provided)."
    • addedInput schema / properties / patch
      Added value: +{
      +  "anyOf": [
      +    {
      +      "items": {
      +        "additionalProperties": true,
      +        "type": "object"
      +      },
      +      "type": "array"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "default": null,
      +  "description": "Structured dashboard edits: up to 100 JSON Patch add, remove, replace or test operations using RFC 6901 paths. Use /- to append to an array; escape ~ as ~0 and / as ~1 in keys. Requires config_hash. Mutually exclusive with config and python_transform. Update title/icon/require_admin/show_in_sidebar in a separate call. Strings in value are preserved literally."
      +}
    • changedInput schema / properties / python_transform / description
      Previous value: -"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\nPYTHON TRANSFORM SECURITY:\n\n✅ ALLOWED:\n- Dictionary/list access: config['views'][0]['cards'][1]\n- Slicing: config['views'][0]['cards'][1:3]\n- Assignment: config['key'] = 'value'\n- Deletion: del config['key'] or config.pop('key')\n- List methods: append, insert, pop, remove, clear, extend\n- Dict methods: update, get, setdefault, keys, values, items\n- Loops: for, if/else, pass, break, continue\n- Comprehensions: [x for x in ...], {k: v for ...}, (x for x in ...)\n- Ternary: x if condition else y\n- Iterable unpacking (* in calls/literals): f(*xs), [*xs, y]\n- Dict unpacking (**) in calls and dict literals: {**d, 'k': v}\n- Keyword arguments: func(key=value)\n- Lambdas (e.g. for `key=`): sorted(items, key=lambda x: x['score'])\n- String methods: startswith, endswith, lower, upper, strip, split, join, replace\n- Safe builtins: isinstance, len, range, enumerate, zip, sorted, reversed,\n  min, max, sum, abs, any, all, round, str, int, float, bool, list, dict,\n  tuple, set\n\n❌ FORBIDDEN:\n- Imports: import, from, __import__\n- File operations: open, read, write\n- Dunder access: __class__, __bases__, __subclasses__\n- Dangerous builtins: eval, exec, compile, getattr, setattr, delattr, hasattr\n- Function definitions: def, class\n- Exception handling: try/except (validate with isinstance/in/.get() instead)\n- While loops: use bounded for loops or comprehensions instead\n\n🎯 PATTERNS:\n- Filter cards: cards = [c for c in cards if keep(c)]\n- Skip in a loop: prefer `continue` over an empty `pass` branch (clearer)\n- Conditionally include: build a new list and `.append(x)` only the\n  cards you want, instead of iterating the original and using if/pass\n  branches to drop entries\n- Modify in place when possible (single pass, fewer surprises) over\n  reconstructing the entire list"New value: +"Python expression to transform existing dashboard config. Mutually exclusive with config and patch. 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\nPYTHON TRANSFORM SECURITY:\n\n✅ ALLOWED:\n- Dictionary/list access: config['views'][0]['cards'][1]\n- Slicing: config['views'][0]['cards'][1:3]\n- Assignment: config['key'] = 'value'\n- Deletion: del config['key'] or config.pop('key')\n- List methods: append, insert, pop, remove, clear, extend\n- Dict methods: update, get, setdefault, keys, values, items\n- Loops: for, if/else, pass, break, continue\n- Comprehensions: [x for x in ...], {k: v for ...}, (x for x in ...)\n- Ternary: x if condition else y\n- Iterable unpacking (* in calls/literals): f(*xs), [*xs, y]\n- Dict unpacking (**) in calls and dict literals: {**d, 'k': v}\n- Keyword arguments: func(key=value)\n- Lambdas (e.g. for `key=`): sorted(items, key=lambda x: x['score'])\n- String methods: startswith, endswith, lower, upper, strip, split, join, replace\n- Safe builtins: isinstance, len, range, enumerate, zip, sorted, reversed,\n  min, max, sum, abs, any, all, round, str, int, float, bool, list, dict,\n  tuple, set\n\n❌ FORBIDDEN:\n- Imports: import, from, __import__\n- File operations: open, read, write\n- Dunder access: __class__, __bases__, __subclasses__\n- Dangerous builtins: eval, exec, compile, getattr, setattr, delattr, hasattr\n- Function definitions: def, class\n- Exception handling: try/except (validate with isinstance/in/.get() instead)\n- While loops: use bounded for loops or comprehensions instead\n\n🎯 PATTERNS:\n- Filter cards: cards = [c for c in cards if keep(c)]\n- Skip in a loop: prefer `continue` over an empty `pass` branch (clearer)\n- Conditionally include: build a new list and `.append(x)` only the\n  cards you want, instead of iterating the original and using if/pass\n  branches to drop entries\n- Modify in place when possible (single pass, fewer surprises) over\n  reconstructing the entire list"
  2. Changed1 schema field changedv8.4.1
    • changedInput schema / properties / return_screenshot / description
      Previous value: -"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."New value: +"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. For visual re-checks after the write (no config round-trip), use the dedicated ha_get_dashboard_screenshot tool instead."
  3. First observedv7.14.2

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only provide destructiveHint=true, so the description carries the burden of behavioral disclosure. It warns about index shifts after delete/add, requires fresh config_hash, explains that move/copy are unsupported in patch, states metadata updates cannot be combined with patch/python_transform, notes strategy dashboards cannot be converted, and details the storage-mode vs YAML-mode limitation. This far exceeds what annotations convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but well-organized with clear headers, code examples, and front-loaded critical requirements ('MUST call ha_get_skill_guide...'). Some content, such as the extensive Python security whitelist and modern dashboard best practices, could be trimmed or moved to the referenced skill guide, but the structure makes key information easy to locate.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex write tool with no output schema, this description is remarkably complete: it covers prerequisites, mode selection, patch syntax, python_transform security, metadata handling, entity ID discovery, screenshot behavior, and YAML-mode exclusions. An agent has all the context needed to invoke the tool correctly and avoid common pitfalls.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Despite 92% schema coverage, the description adds substantial parameter-level meaning: it gives worked examples for python_transform expressions, explains patch path syntax and escape rules, clarifies config_hash requirements per mode, documents metadata-only calls, and elaborates on the BestPracticeKey read-receipt protocol. This goes well beyond the baseline for high schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description opens with a clear, specific verb+resource statement: 'Create or update a Home Assistant dashboard.' It further distinguishes itself from related siblings by referencing ha_config_get_dashboard for reading config and ha_get_dashboard_screenshot for visual checks, and the detailed mode breakdown (patch/python_transform/config) makes the tool's scope unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly states when to use each mode under 'WHEN TO USE WHICH MODE' and gives alternative-tool guidance: use ha_config_get_dashboard to get fresh config_hash after index-shifting operations, use ha_get_dashboard_screenshot for visual re-checks instead of return_screenshot, and use ha_get_overview/ha_search for entity discovery. It also clearly excludes YAML-mode dashboards and routes to ha_config_set_yaml for registration edits.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Deploy Server

Other Tools