1616from pydantic import Field
1717
1818from ..config import get_global_settings
19- from ..utils .python_sandbox import PythonSandboxError , get_security_documentation , safe_execute
19+ from ..utils .python_sandbox import (
20+ PythonSandboxError ,
21+ get_security_documentation ,
22+ safe_execute ,
23+ )
2024from .helpers import log_tool_usage
2125from .util_helpers import parse_json_param
2226
2529# Try to import jq - it's not available on Windows ARM64
2630try :
2731 import jq # noqa: F401 - Used to check availability, re-imported in function
32+
2833 JQ_AVAILABLE = True
2934except ImportError :
3035 JQ_AVAILABLE = False
@@ -98,7 +103,9 @@ async def _verify_config_unchanged(
98103 get_data ["url_path" ] = url_path
99104
100105 result = await client .send_websocket_message (get_data )
101- current_config = result .get ("result" , result ) if isinstance (result , dict ) else result
106+ current_config = (
107+ result .get ("result" , result ) if isinstance (result , dict ) else result
108+ )
102109
103110 if not isinstance (current_config , dict ):
104111 return {"success" : True } # Can't verify, proceed anyway
@@ -193,29 +200,33 @@ def _find_cards_in_config(
193200 if not isinstance (card , dict ):
194201 continue
195202 if _card_matches (card , entity_id , card_type , heading ):
196- matches .append ({
197- "view_index" : view_idx ,
198- "section_index" : section_idx ,
199- "card_index" : card_idx ,
200- "jq_path" : f".views[{ view_idx } ].sections[{ section_idx } ].cards[{ card_idx } ]" ,
201- "card_type" : card .get ("type" ),
202- "card_config" : card ,
203- })
203+ matches .append (
204+ {
205+ "view_index" : view_idx ,
206+ "section_index" : section_idx ,
207+ "card_index" : card_idx ,
208+ "jq_path" : f".views[{ view_idx } ].sections[{ section_idx } ].cards[{ card_idx } ]" ,
209+ "card_type" : card .get ("type" ),
210+ "card_config" : card ,
211+ }
212+ )
204213 else :
205214 # Flat view (masonry, panel, sidebar)
206215 cards = view .get ("cards" , [])
207216 for card_idx , card in enumerate (cards ):
208217 if not isinstance (card , dict ):
209218 continue
210219 if _card_matches (card , entity_id , card_type , heading ):
211- matches .append ({
212- "view_index" : view_idx ,
213- "section_index" : None ,
214- "card_index" : card_idx ,
215- "jq_path" : f".views[{ view_idx } ].cards[{ card_idx } ]" ,
216- "card_type" : card .get ("type" ),
217- "card_config" : card ,
218- })
220+ matches .append (
221+ {
222+ "view_index" : view_idx ,
223+ "section_index" : None ,
224+ "card_index" : card_idx ,
225+ "jq_path" : f".views[{ view_idx } ].cards[{ card_idx } ]" ,
226+ "card_type" : card .get ("type" ),
227+ "card_config" : card ,
228+ }
229+ )
219230
220231 return matches
221232
@@ -239,8 +250,7 @@ def _card_matches(
239250 card_entities = card .get ("entities" , [])
240251 if isinstance (card_entities , list ):
241252 all_entities = [card_entity ] + [
242- e .get ("entity" , e ) if isinstance (e , dict ) else e
243- for e in card_entities
253+ e .get ("entity" , e ) if isinstance (e , dict ) else e for e in card_entities
244254 ]
245255 else :
246256 all_entities = [card_entity ]
@@ -362,7 +372,9 @@ async def ha_config_get_dashboard(
362372 config = response .get ("result" ) if isinstance (response , dict ) else response
363373
364374 # Compute hash for optimistic locking in subsequent operations
365- config_hash = _compute_config_hash (config ) if isinstance (config , dict ) else None
375+ config_hash = (
376+ _compute_config_hash (config ) if isinstance (config , dict ) else None
377+ )
366378
367379 # Calculate config size for progressive disclosure hint
368380 config_size = len (json .dumps (config )) if isinstance (config , dict ) else 0
@@ -411,8 +423,9 @@ async def ha_config_set_dashboard(
411423 url_path : Annotated [
412424 str ,
413425 Field (
414- description = "Unique URL path for dashboard (must contain hyphen, "
415- "e.g., 'my-dashboard', 'mobile-view')"
426+ description = "Dashboard URL path (e.g., 'my-dashboard'). "
427+ "Use 'default' or 'lovelace' for the default dashboard. "
428+ "New dashboards must use a hyphenated path."
416429 ),
417430 ],
418431 config : Annotated [
@@ -430,7 +443,7 @@ async def ha_config_set_dashboard(
430443 description = "jq expression to transform existing dashboard config. "
431444 "Mutually exclusive with config and python_transform. Requires config_hash for validation. "
432445 "Examples: '.views[0].sections[1].cards[0].icon = \" mdi:thermometer\" ', "
433- "' .views[0].cards += [{\ " type\ " : \ " button\ " , \ " entity\ " : \ " light.bedroom\ " }]', "
446+ ' \' .views[0].cards += [{"type": "button", "entity": "light.bedroom"}]\ ' , '
434447 "'del(.views[0].sections[0].cards[2])'. "
435448 "MULTI-OP: Chain with '|': 'del(.views[0].cards[2]) | .views[0].cards[0].icon = \" mdi:new\" '. "
436449 "Use ha_dashboard_find_card() to get jq_path for targeted edits."
@@ -447,8 +460,7 @@ async def ha_config_set_dashboard(
447460 "Simple: python_transform=\" config['views'][0]['cards'][0]['icon'] = 'mdi:lamp'\" "
448461 "Pattern: python_transform=\" for card in config['views'][0]['cards']: if 'light' in card.get('entity', ''): card['icon'] = 'mdi:lightbulb'\" "
449462 "Multi-op: python_transform=\" config['views'][0]['cards'][0]['icon'] = 'mdi:lamp'; del config['views'][0]['cards'][2]\" "
450- "\n \n "
451- + get_security_documentation (),
463+ "\n \n " + get_security_documentation (),
452464 ),
453465 ] = None ,
454466 config_hash : Annotated [
@@ -483,7 +495,8 @@ async def ha_config_set_dashboard(
483495 Creates a new dashboard or updates an existing one with the provided configuration.
484496 Supports three modes: full config replacement, Python transformation, OR jq-based transformation.
485497
486- IMPORTANT: url_path must contain a hyphen (-) to be valid.
498+ Use 'default' or 'lovelace' to target the built-in default dashboard.
499+ New dashboards require a hyphenated url_path (e.g., 'my-dashboard').
487500
488501 WHEN TO USE WHICH MODE:
489502 - python_transform: RECOMMENDED for edits. Surgical/pattern-based updates, works on all platforms.
@@ -601,15 +614,22 @@ async def ha_config_set_dashboard(
601614 (title, icon), use ha_config_update_dashboard_metadata().
602615 """
603616 try :
604- # Validate url_path contains hyphen
605- if "-" not in url_path :
617+ # Handle "default" as alias for the default dashboard
618+ # (matches ha_config_get_dashboard behavior)
619+ if url_path == "default" :
620+ url_path = "lovelace"
621+
622+ # Validate url_path contains hyphen for new dashboards
623+ # The built-in "lovelace" dashboard is exempt since it already exists
624+ if "-" not in url_path and url_path != "lovelace" :
606625 return {
607626 "success" : False ,
608627 "action" : "set" ,
609628 "error" : "url_path must contain a hyphen (-)" ,
610629 "suggestions" : [
611630 f"Try '{ url_path .replace ('_' , '-' )} ' instead" ,
612631 "Use format like 'my-dashboard' or 'mobile-view'" ,
632+ "Use 'lovelace' or 'default' to edit the default dashboard" ,
613633 ],
614634 }
615635
@@ -728,7 +748,9 @@ async def ha_config_set_dashboard(
728748
729749 save_result = await client .send_websocket_message (save_data )
730750
731- if isinstance (save_result , dict ) and not save_result .get ("success" , True ):
751+ if isinstance (save_result , dict ) and not save_result .get (
752+ "success" , True
753+ ):
732754 error_msg = save_result .get ("error" , {})
733755 if isinstance (error_msg , dict ):
734756 error_msg = error_msg .get ("message" , str (error_msg ))
@@ -793,14 +815,18 @@ async def ha_config_set_dashboard(
793815 ],
794816 }
795817
796- current_config = response .get ("result" ) if isinstance (response , dict ) else response
818+ current_config = (
819+ response .get ("result" ) if isinstance (response , dict ) else response
820+ )
797821 if not isinstance (current_config , dict ):
798822 return {
799823 "success" : False ,
800824 "action" : "jq_transform" ,
801825 "url_path" : url_path ,
802826 "error" : "Current dashboard config is invalid" ,
803- "suggestions" : ["Initialize dashboard with 'config' parameter first" ],
827+ "suggestions" : [
828+ "Initialize dashboard with 'config' parameter first"
829+ ],
804830 }
805831
806832 # Validate config_hash for optimistic locking
@@ -819,7 +845,9 @@ async def ha_config_set_dashboard(
819845 }
820846
821847 # Apply jq transformation
822- transformed_config , error = _apply_jq_transform (current_config , jq_transform )
848+ transformed_config , error = _apply_jq_transform (
849+ current_config , jq_transform
850+ )
823851 if error :
824852 return {
825853 "success" : False ,
@@ -843,7 +871,9 @@ async def ha_config_set_dashboard(
843871
844872 save_result = await client .send_websocket_message (save_data )
845873
846- if isinstance (save_result , dict ) and not save_result .get ("success" , True ):
874+ if isinstance (save_result , dict ) and not save_result .get (
875+ "success" , True
876+ ):
847877 error_msg = save_result .get ("error" , {})
848878 if isinstance (error_msg , dict ):
849879 error_msg = error_msg .get ("message" , str (error_msg ))
@@ -860,7 +890,9 @@ async def ha_config_set_dashboard(
860890
861891 # Compute new hash for potential chaining
862892 # transformed_config is guaranteed to be a dict here (validated above)
863- new_config_hash = _compute_config_hash (cast (dict [str , Any ], transformed_config ))
893+ new_config_hash = _compute_config_hash (
894+ cast (dict [str , Any ], transformed_config )
895+ )
864896
865897 return {
866898 "success" : True ,
@@ -885,6 +917,11 @@ async def ha_config_set_dashboard(
885917 d .get ("url_path" ) == url_path for d in existing_dashboards
886918 )
887919
920+ # The built-in default dashboard ("lovelace") is always present
921+ # but isn't listed by lovelace/dashboards/list on fresh installs
922+ if url_path == "lovelace" :
923+ dashboard_exists = True
924+
888925 # If dashboard doesn't exist, create it
889926 dashboard_id = None
890927 if not dashboard_exists :
@@ -950,7 +987,10 @@ async def ha_config_set_dashboard(
950987 # For existing dashboards, optionally validate config_hash and warn on large replacement
951988 if dashboard_exists :
952989 # Fetch current config for validation/comparison
953- get_data : dict [str , Any ] = {"type" : "lovelace/config" , "force" : True }
990+ get_data : dict [str , Any ] = {
991+ "type" : "lovelace/config" ,
992+ "force" : True ,
993+ }
954994 if url_path :
955995 get_data ["url_path" ] = url_path
956996 current_response = await client .send_websocket_message (get_data )
@@ -1039,7 +1079,7 @@ async def ha_config_set_dashboard(
10391079 "error" : str (e ),
10401080 "suggestions" : [
10411081 "Ensure url_path is unique (not already in use for different dashboard type)" ,
1042- "Verify url_path contains a hyphen " ,
1082+ "New dashboards require a hyphenated url_path " ,
10431083 "Check that you have admin permissions" ,
10441084 "Verify config format is valid Lovelace JSON" ,
10451085 ],
@@ -1446,7 +1486,6 @@ async def ha_get_card_documentation(
14461486 "error" : str (e ),
14471487 }
14481488
1449-
14501489 # =========================================================================
14511490 # Dashboard Resource Management Tools
14521491 # =========================================================================
@@ -1483,7 +1522,9 @@ async def ha_get_card_documentation(
14831522 async def ha_dashboard_find_card (
14841523 url_path : Annotated [
14851524 str | None ,
1486- Field (description = "Dashboard URL path, e.g. 'lovelace-home'. Omit for default." ),
1525+ Field (
1526+ description = "Dashboard URL path, e.g. 'lovelace-home'. Omit for default."
1527+ ),
14871528 ] = None ,
14881529 entity_id : Annotated [
14891530 str | None ,
@@ -1505,7 +1546,9 @@ async def ha_dashboard_find_card(
15051546 ] = None ,
15061547 include_config : Annotated [
15071548 bool ,
1508- Field (description = "Include full card configuration in results (increases output size)." ),
1549+ Field (
1550+ description = "Include full card configuration in results (increases output size)."
1551+ ),
15091552 ] = False ,
15101553 ) -> dict [str , Any ]:
15111554 """
@@ -1590,7 +1633,9 @@ async def ha_dashboard_find_card(
15901633 "action" : "find_card" ,
15911634 "url_path" : url_path ,
15921635 "error" : "Dashboard config is empty or invalid" ,
1593- "suggestions" : ["Initialize dashboard with ha_config_set_dashboard" ],
1636+ "suggestions" : [
1637+ "Initialize dashboard with ha_config_set_dashboard"
1638+ ],
15941639 }
15951640
15961641 # Check for strategy dashboard
@@ -1630,7 +1675,8 @@ async def ha_dashboard_find_card(
16301675 "matches" : matches ,
16311676 "match_count" : len (matches ),
16321677 "hint" : "Use jq_path with ha_config_set_dashboard(jq_transform=...) for targeted updates"
1633- if matches else "No matches found. Try broader search criteria." ,
1678+ if matches
1679+ else "No matches found. Try broader search criteria." ,
16341680 }
16351681
16361682 except asyncio .CancelledError :
@@ -1653,4 +1699,3 @@ async def ha_dashboard_find_card(
16531699 "Verify dashboard with ha_config_get_dashboard(list_only=True)" ,
16541700 ],
16551701 }
1656-
0 commit comments