Compare commits

...
10 Commits
349 changed files with 9907 additions and 10089 deletions
+1 -1
View File
@@ -1 +1 @@
2026.7.2
2026.7.3
+25 -13
View File
@@ -1,14 +1,26 @@
# Home Assistant Core Exclusions
*.db
*.db-shm
*.db-wal
*.log
.storage/
# Only track specific file types
# Ignore everything by default
*
# === Home Assistant Version Control Managed (do not edit below) ===
# Tracked file types
!*.yaml
!**/.??*.yaml
!*.yml
!**/.??*.yml
# Track specific .storage configuration files
!.storage/lovelace
!.storage/lovelace_dashboards
!.storage/lovelace_resources
!.storage/lovelace.*
# === End Home Assistant Version Control Managed ===
# Allow directory traversal
!*/
# Re-ignore macOS metadata files (even if they match allowed extensions)
._*
# Excluded files
secrets.yaml
*icon*.png
*state.json
# Custom integrations and private tokens
.google_nest_prompt_info.json
nest_protect_cookies.json
+7
View File
@@ -7,3 +7,10 @@
{"timestamp": "2026-07-16T14:58:34.806520+00:00", "tool_name": "ha_list_floors_areas", "parameters": {"fields": null, "area_fields": null}, "execution_time_ms": 34.44170951843262, "success": true, "error_message": null, "response_size_bytes": 3757, "user_context": null}
{"timestamp": "2026-07-16T15:17:46.049602+00:00", "tool_name": "ha_get_overview", "parameters": {"detail_level": "minimal", "domains": null, "limit": null, "offset": 0, "max_entities_per_domain": null, "include_state": null, "include_entity_id": null, "include_notifications": true, "include_dismissed_repairs": false, "fields": null}, "execution_time_ms": 1456.7010402679443, "success": true, "error_message": null, "response_size_bytes": 14815, "user_context": null}
{"timestamp": "2026-07-16T15:20:28.060870+00:00", "tool_name": "ha_search", "parameters": {"query": null, "domain_filter": "automation", "area_filter": null, "search_types": null, "limit": 10, "offset": 0, "exact_match": true, "include_hidden": true, "include_config": false, "group_by_domain": false, "per_domain_limit": null, "state_filter": null, "result_fields": null, "fields": null, "config_time_budget": null, "ctx": {"_STATE_TTL_SECONDS": 86400}}, "execution_time_ms": 113.4181022644043, "success": true, "error_message": null, "response_size_bytes": 2476, "user_context": null}
{"timestamp": "2026-07-23T19:05:20.540787+00:00", "tool_name": "ha_search", "parameters": {"query": "office lamp", "domain_filter": "light", "area_filter": null, "search_types": null, "limit": 10, "offset": 0, "exact_match": true, "include_hidden": true, "include_config": false, "group_by_domain": false, "per_domain_limit": null, "state_filter": null, "result_fields": null, "fields": null, "config_time_budget": null, "ctx": {"_STATE_TTL_SECONDS": 86400}}, "execution_time_ms": 151.7007350921631, "success": true, "error_message": null, "response_size_bytes": 911, "user_context": null}
{"timestamp": "2026-07-23T19:05:26.978049+00:00", "tool_name": "ha_config_set_automation", "parameters": {"config": {"actions": [{"action": "light.turn_on", "target": {"entity_id": "light.office_lamp"}}], "alias": "Office Lamp On at 6PM", "description": "Turn on the office lamp every day at 6:00 PM", "mode": "single", "triggers": [{"at": "18:00:00", "trigger": "time"}]}, "identifier": null, "python_transform": null, "config_hash": null, "category": null, "wait": true, "MandatoryBPS": true, "BestPracticeKey": null}, "execution_time_ms": 844.0155982971191, "success": true, "error_message": null, "response_size_bytes": 59574, "user_context": null}
{"timestamp": "2026-07-23T19:06:27.496061+00:00", "tool_name": "ha_search", "parameters": {"query": "entryway motion", "domain_filter": "binary_sensor", "area_filter": null, "search_types": null, "limit": 10, "offset": 0, "exact_match": true, "include_hidden": true, "include_config": false, "group_by_domain": false, "per_domain_limit": null, "state_filter": null, "result_fields": null, "fields": null, "config_time_budget": null, "ctx": {"_STATE_TTL_SECONDS": 86400}}, "execution_time_ms": 113.06405067443848, "success": true, "error_message": null, "response_size_bytes": 779, "user_context": null}
{"timestamp": "2026-07-23T19:06:38.734934+00:00", "tool_name": "ha_search", "parameters": {"query": "entryway motion", "domain_filter": null, "area_filter": null, "search_types": null, "limit": 10, "offset": 0, "exact_match": false, "include_hidden": true, "include_config": false, "group_by_domain": false, "per_domain_limit": null, "state_filter": null, "result_fields": null, "fields": null, "config_time_budget": null, "ctx": {"_STATE_TTL_SECONDS": 86400}}, "execution_time_ms": 7804.042100906372, "success": true, "error_message": null, "response_size_bytes": 4501, "user_context": null}
{"timestamp": "2026-07-23T19:07:02.132122+00:00", "tool_name": "ha_config_get_automation", "parameters": {"identifier": "automation.entryway_lamp_temporary"}, "execution_time_ms": 86.78007125854492, "success": true, "error_message": null, "response_size_bytes": 633, "user_context": null}
{"timestamp": "2026-07-23T19:07:06.747984+00:00", "tool_name": "ha_config_get_automation", "parameters": {"identifier": "automation.office_lamp_on_at_6pm"}, "execution_time_ms": 103.78479957580566, "success": true, "error_message": null, "response_size_bytes": 413, "user_context": null}
{"timestamp": "2026-07-23T19:07:15.081115+00:00", "tool_name": "ha_config_set_automation", "parameters": {"config": null, "identifier": "automation.office_lamp_on_at_6pm", "python_transform": "config['triggers'].append({'trigger': 'state', 'entity_id': ['binary_sensor.entryway'], 'to': ['on'], 'id': 'entryway_motion'})\nconfig['description'] = 'Turn on the office lamp every day at 6:00 PM, or whenever motion is detected in the entryway'", "config_hash": "5f51e207357bbf3f", "category": null, "wait": true, "MandatoryBPS": true, "BestPracticeKey": null}, "execution_time_ms": 350.6944179534912, "success": true, "error_message": null, "response_size_bytes": 59968, "user_context": null}
+1 -1
View File
@@ -1 +1 @@
{"pid": 71, "version": 1, "ha_version": "2026.7.2", "start_ts": 1784644362.8406641}
{"pid": 71, "version": 1, "ha_version": "2026.7.3", "start_ts": 1784830467.7871833}
+205
View File
@@ -0,0 +1,205 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace.alarm_section",
"data": {
"config": {
"views": [
{
"type": "sections",
"sections": [
{
"type": "grid",
"cards": [
{
"type": "heading",
"heading_style": "title",
"heading": "Sensors"
},
{
"type": "entities",
"entities": [
{
"entity": "binary_sensor.entryway"
},
{
"entity": "binary_sensor.basement"
},
{
"entity": "binary_sensor.office_occupancy"
},
{
"entity": "binary_sensor.downstairs_bathroom_occupancy"
},
{
"entity": "binary_sensor.night_light_occupancy"
},
{
"entity": "binary_sensor.front_door"
},
{
"entity": "binary_sensor.patio_door"
},
{
"entity": "binary_sensor.office_door_contact"
},
{
"entity": "binary_sensor.garage_door_contact"
},
{
"entity": "binary_sensor.bedroom_window"
},
{
"entity": "binary_sensor.seraphine_window"
},
{
"entity": "binary_sensor.bedroom_window"
},
{
"entity": "binary_sensor.side_window_1"
},
{
"entity": "binary_sensor.side_window_2"
},
{
"entity": "binary_sensor.swimming_pool_window"
},
{
"entity": "input_boolean.blink_backyard_camera_motion"
},
{
"entity": "input_boolean.blink_front_camera_motion"
},
{
"entity": "input_boolean.blink_shed_camera_motion"
},
{
"entity": "input_boolean.blink_inside_shed_camera_motion"
}
]
}
]
},
{
"type": "grid",
"cards": [
{
"type": "heading",
"heading_style": "title",
"heading": "Cameras"
},
{
"square": false,
"type": "grid",
"cards": [
{
"type": "tile",
"entity": "camera.front_door_front_door",
"show_entity_picture": true
},
{
"type": "tile",
"entity": "camera.entryway_entryway_camera",
"show_entity_picture": true,
"vertical": false,
"features_position": "bottom"
},
{
"type": "tile",
"entity": "camera.living_room_living_room_camera",
"show_entity_picture": true,
"vertical": false,
"features_position": "bottom"
},
{
"type": "tile",
"entity": "camera.hallway_hallway_camera",
"show_entity_picture": true,
"vertical": false,
"features_position": "bottom"
}
],
"columns": 2
},
{
"type": "history-graph",
"entities": [
{
"entity": "input_boolean.blink_backyard_camera_motion"
},
{
"entity": "input_boolean.blink_shed_camera_motion"
},
{
"entity": "input_boolean.blink_inside_shed_camera_motion"
},
{
"entity": "input_boolean.blink_front_camera_motion"
},
{
"entity": "binary_sensor.office_occupancy"
},
{
"entity": "binary_sensor.downstairs_bathroom_occupancy"
},
{
"entity": "binary_sensor.night_light_occupancy"
},
{
"entity": "binary_sensor.basement"
},
{
"entity": "binary_sensor.entryway"
},
{
"entity": "binary_sensor.garage_door_contact"
}
],
"title": "Motion Senors",
"hours_to_show": 6,
"logarithmic_scale": false,
"expand_legend": false
},
{
"type": "history-graph",
"entities": [
{
"entity": "sensor.night_light_illuminance"
},
{
"entity": "light.night_light"
}
],
"hours_to_show": 24
},
{
"type": "custom:mushroom-entity-card",
"entity": "input_boolean.office_motion_override",
"name": "Office Motion Override",
"icon": "mdi:home-account",
"icon_color": "green",
"secondary_info": "state",
"tap_action": {
"action": "toggle"
},
"hold_action": {
"action": "none"
}
},
{
"type": "tile",
"entity": "alarm_control_panel.blainville_alarm",
"features": [
{
"type": "alarm-modes"
}
]
}
]
}
]
}
]
}
}
}
File diff suppressed because it is too large Load Diff
+126
View File
@@ -0,0 +1,126 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace.dashboard_music",
"data": {
"config": {
"views": [
{
"type": "sections",
"sections": [
{
"type": "grid",
"cards": [
{
"type": "heading",
"heading": "Amazon Devices",
"heading_style": "title"
},
{
"type": "vertical-stack",
"cards": [
{
"type": "entities",
"title": "Alexa Music Router",
"show_header_toggle": false,
"entities": [
{
"entity": "input_select.alexa_playlist",
"name": "Select Playlist"
},
{
"entity": "input_select.alexa_target_device",
"name": "Target Speaker"
},
{
"entity": "input_number.alexa_master_volume",
"name": "Speaker Volume"
}
]
},
{
"type": "grid",
"columns": 2,
"square": false,
"cards": [
{
"type": "button",
"name": "Play Music",
"icon": "mdi:play",
"tap_action": {
"action": "call-service",
"service": "script.dynamic_alexa_spotify_player"
}
},
{
"type": "button",
"name": "Stop Music",
"icon": "mdi:stop",
"tap_action": {
"action": "call-service",
"service": "script.stop_alexa_music"
}
}
]
}
]
}
]
},
{
"type": "grid",
"cards": [
{
"type": "heading",
"heading": "Office Speaker",
"heading_style": "title"
},
{
"type": "custom:mass-player-card",
"entities": [
"media_player.office_speaker_2",
"media_player.francos_macbook_pro"
],
"panel": true,
"sync_player_across_dashboard": true,
"media_browser": {
"recommendations": {}
}
}
]
},
{
"type": "grid",
"cards": [
{
"type": "custom:my-music-library-card",
"entity": "media_player.office_speaker_2",
"grid_options": {
"columns": 12,
"rows": "auto"
}
}
],
"column_span": 1
},
{
"type": "grid",
"cards": [
{
"type": "iframe",
"url": "/d5369777_music_assistant",
"aspect_ratio": "75%",
"grid_options": {
"columns": "full",
"rows": 8
}
}
],
"column_span": 2
}
]
}
]
}
}
}
+538
View File
@@ -0,0 +1,538 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace.dashboard_temp",
"data": {
"config": {
"views": [
{
"type": "sections",
"sections": [
{
"type": "grid",
"cards": [
{
"type": "custom:uptime-card",
"entity": "sensor.nas_status",
"name": "NAS Availability",
"icon": "mdi:nas",
"ok": [
"up",
"Up",
"online"
],
"ko": [
"down",
"Down",
"offline"
],
"alias": {
"ok": "Up",
"ko": "Down"
},
"show_state": true,
"bar": {
"amount": 30
},
"title_adaptive_color": true,
"status_adaptive_color": false,
"icon_adaptive_color": true
},
{
"type": "custom:uptime-card",
"entity": "sensor.homeassistant_status",
"name": "NAS Availability",
"icon": "mdi:nas",
"ok": [
"up",
"Up",
"online"
],
"ko": [
"down",
"Down",
"offline"
],
"alias": {
"ok": "Up",
"ko": "Down"
},
"show_state": true,
"bar": {
"amount": 30
},
"title_adaptive_color": true,
"status_adaptive_color": false,
"icon_adaptive_color": true
},
{
"type": "custom:uptime-card",
"entity": "sensor.ollama_status",
"name": "NAS Availability",
"icon": "mdi:nas",
"ok": [
"up",
"Up",
"online"
],
"ko": [
"down",
"Down",
"offline"
],
"alias": {
"ok": "Up",
"ko": "Down"
},
"show_state": true,
"bar": {
"amount": 30
},
"title_adaptive_color": true,
"status_adaptive_color": false,
"icon_adaptive_color": true
},
{
"type": "custom:uptime-card",
"entity": "sensor.next_cloud_status",
"name": "NAS Availability",
"icon": "mdi:nas",
"ok": [
"up",
"Up",
"online"
],
"ko": [
"down",
"Down",
"offline"
],
"alias": {
"ok": "Up",
"ko": "Down"
},
"show_state": true,
"bar": {
"amount": 30
},
"title_adaptive_color": true,
"status_adaptive_color": false,
"icon_adaptive_color": true
},
{
"type": "sensor",
"entity": "sensor.nas_response_time",
"name": "NAS Latency",
"graph": "line",
"hours_to_show": 24
},
{
"type": "sensor",
"entity": "sensor.homeassistant_response_time",
"name": "HA Latency",
"graph": "line",
"hours_to_show": 24
},
{
"graph": "line",
"type": "sensor",
"entity": "sensor.ollama_response_time",
"hours_to_show": 24,
"detail": 1
},
{
"graph": "line",
"type": "sensor",
"entity": "sensor.next_cloud_response_time",
"hours_to_show": 24,
"detail": 1
},
{
"graph": "line",
"type": "sensor",
"entity": "sensor.homeassistant_certificate_expiry",
"hours_to_show": 24,
"detail": 1
},
{
"graph": "line",
"type": "sensor",
"entity": "sensor.next_cloud_certificate_expiry",
"hours_to_show": 24,
"detail": 1
}
]
},
{
"type": "grid",
"cards": [
{
"type": "heading",
"heading": "HVAC Stats",
"heading_style": "title"
},
{
"type": "grid",
"square": false,
"columns": 1,
"cards": [
{
"type": "statistics-graph",
"title": "Daily Breakdown (Last 7 Days)",
"chart_type": "bar",
"period": "day",
"days_to_show": 7,
"stat_types": [
"change"
],
"entities": [
"sensor.nest_cooling_time_stats",
"sensor.nest_heating_time_stats"
]
},
{
"type": "statistics-graph",
"title": "Monthly Trend (Last 6 Months)",
"chart_type": "line",
"period": "month",
"days_to_show": 180,
"stat_types": [
"max",
"change"
],
"entities": [
"sensor.nest_cooling_monthly",
"sensor.nest_heating_monthly"
]
},
{
"type": "statistics-graph",
"title": "Yearly Comparison",
"chart_type": "bar",
"period": "month",
"days_to_show": 365,
"stat_types": [
"max",
"change"
],
"entities": [
"sensor.nest_cooling_yearly",
"sensor.nest_heating_yearly"
]
}
]
},
{
"type": "statistics-graph",
"chart_type": "bar",
"period": "day",
"days_to_show": 7,
"stat_types": [
"change"
],
"entities": [
{
"entity": "sensor.nest_cooling_weekly",
"name": "Cooling"
},
{
"entity": "sensor.nest_heating_weekly",
"name": "Heating"
}
],
"title": "Weekly HVAC Usage Breakdown"
},
{
"type": "heading",
"heading": "Dishwasher"
},
{
"type": "tile",
"entity": "binary_sensor.150633095332665_door",
"icon": "mdi:door-open",
"vertical": false,
"features_position": "bottom"
},
{
"type": "tile",
"entity": "sensor.150633095332665_status"
},
{
"type": "tile",
"entity": "sensor.150633095332665_progress"
},
{
"type": "tile",
"entity": "binary_sensor.150633095332665_rinse_aid"
},
{
"type": "tile",
"entity": "sensor.150633095332665_temperature"
},
{
"type": "tile",
"entity": "sensor.150633095332665_mode"
},
{
"type": "tile",
"entity": "sensor.150633095332665_bright"
},
{
"type": "tile",
"entity": "sensor.150633095332665_error_code"
},
{
"type": "tile",
"entity": "sensor.150633095332665_softwater"
},
{
"type": "tile",
"entity": "sensor.150633095332665_time_remaining"
},
{
"type": "tile",
"entity": "binary_sensor.150633095332665_salt"
},
{
"type": "tile",
"entity": "counter.dishwasher_wash_cycles"
},
{
"type": "tile",
"entity": "counter.dishwasher_total_wash_cycles"
}
]
},
{
"type": "grid",
"cards": [
{
"type": "heading",
"heading": "New section"
},
{
"type": "custom:album-slideshow-card",
"entity": "camera.album_slideshow_google_screensaver"
},
{
"type": "entities",
"title": "Network Status",
"entities": [
{
"entity": "sensor.gt_ax11000_connected_devices",
"name": "Connected Clients",
"icon": "mdi:devices"
},
{
"entity": "sensor.gt_ax11000_wan_download_speed",
"name": "Download Speed",
"icon": "mdi:download-network"
},
{
"entity": "sensor.gt_ax11000_wan_upload_speed",
"name": "Upload Speed",
"icon": "mdi:upload-network"
},
{
"entity": "sensor.gt_ax11000_wired_download_speed",
"name": "Wired D/L"
},
{
"entity": "sensor.gt_ax11000_wired_upload_speed",
"name": "Wired U/L"
}
]
},
{
"type": "custom:mini-graph-card",
"title": "Network Traffic (24h)",
"entities": [
{
"entity": "sensor.gt_ax11000_wan_download_speed",
"name": "Download",
"color": "#2196F3"
},
{
"entity": "sensor.gt_ax11000_wan_upload_speed",
"name": "Upload",
"color": "#4CAF50"
}
],
"hours_to_show": 6,
"points_per_hour": 4,
"line_width": 4,
"fill": "fade",
"show": {
"graph": "line",
"fill": true,
"state": false,
"name": false,
"extrema": false,
"average": false
}
},
{
"type": "picture-glance",
"title": "Backyard Shed",
"camera_image": "camera.backyard_shed",
"camera_view": "live",
"entities": [],
"tap_action": {
"action": "call-service",
"service": "camera.turn_on",
"target": {
"entity_id": "camera.backyard_shed"
}
}
},
{
"type": "statistics-graph",
"entities": [
"sensor.gt_ax11000_wan_download_speed",
"sensor.gt_ax11000_wan_upload_speed"
],
"days_to_show": 1,
"period": "hour",
"chart_type": "line",
"stat_types": [
"mean"
]
}
]
},
{
"type": "grid",
"cards": [
{
"type": "heading",
"heading": "New section"
},
{
"type": "custom:treemap-card",
"entities": [
"sensor.*power"
],
"exclude": [
"*meter00*"
]
},
{
"type": "custom:auto-entities",
"card": {
"type": "grid",
"columns": 2,
"square": false
},
"filter": {
"include": [
{
"domain": "light",
"options": {
"type": "custom:mushroom-light-card",
"use_light_color": true,
"show_brightness_control": true,
"show_color_temp_control": true,
"collapsible_controls": true,
"tap_action": {
"action": "toggle"
}
}
}
]
}
}
]
},
{
"type": "grid",
"cards": [
{
"type": "heading",
"heading": "New section"
},
{
"type": "custom:simple-thermostat",
"entity": "climate.living_room_living_room"
},
{
"type": "custom:simple-thermostat",
"entity": "climate.vt_basement"
}
]
},
{
"type": "grid",
"cards": [
{
"type": "heading",
"heading": "Blink Backyard Tree"
},
{
"type": "tile",
"entity": "camera.blink_backyard_tree",
"show_entity_picture": true
},
{
"type": "tile",
"entity": "binary_sensor.blink_backyard_tree_motion"
},
{
"type": "tile",
"entity": "sensor.blink_backyard_tree_temperature"
}
]
}
],
"max_columns": 4,
"title": "Main",
"cards": []
},
{
"type": "panel",
"path": "",
"cards": [
{
"type": "custom:climate-scheduler-card"
}
],
"title": "Climate"
},
{
"type": "sections",
"max_columns": 4,
"title": "TaskMate",
"path": "test",
"sections": [
{
"type": "grid",
"cards": [
{
"type": "custom:taskmate-overview-card",
"entity": "sensor.taskmate_overview",
"title": "TaskMate",
"child": "Seraphine Skyler"
}
]
},
{
"type": "grid",
"cards": [
{
"type": "heading",
"heading": "Overview",
"heading_style": "title"
},
{
"type": "custom:taskmate-calendar-card",
"entity": "sensor.taskmate_overview",
"title": "Task Calendar"
}
]
}
],
"cards": []
}
]
}
}
}
+145
View File
@@ -0,0 +1,145 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace.in_meeting",
"data": {
"config": {
"wallpanel": {
"enabled": true,
"hide_toolbar": false,
"hide_sidebar": true,
"fullscreen": false,
"idle_time": 0
},
"views": [
{
"title": "Main",
"path": "main",
"type": "panel",
"cards": [
{
"type": "grid",
"columns": 2,
"square": false,
"cards": [
{
"type": "vertical-stack",
"cards": [
{
"show_name": true,
"show_icon": true,
"type": "button",
"entity": "input_boolean.in_a_meeting",
"name": "OFFICE MEETING STATUS",
"show_state": true,
"icon": "mdi:door-open",
"icon_height": "200px"
},
{
"type": "conditional",
"conditions": [
{
"condition": "state",
"entity": "input_boolean.in_a_meeting",
"state": "off"
}
],
"card": {
"show_name": true,
"show_icon": false,
"type": "button",
"entity": "input_boolean.in_a_meeting",
"name": "🟢 AVAILABLE - COME IN",
"show_state": false,
"icon_height": "100px"
}
},
{
"type": "conditional",
"conditions": [
{
"condition": "state",
"entity": "input_boolean.in_a_meeting",
"state": "on"
}
],
"card": {
"type": "button",
"entity": "input_boolean.in_a_meeting",
"name": "🔴 DO NOT ENTER - IN A MEETING",
"show_state": false,
"show_icon": false
}
},
{
"type": "entities",
"entities": [
{
"entity": "input_boolean.in_a_meeting",
"name": "Manual Desk Toggle",
"icon": "mdi:power"
}
]
}
]
},
{
"type": "vertical-stack",
"cards": [
{
"type": "markdown",
"title": "Desk Companion",
"content": "### System Status This dashboard displays your live office configuration. Outside of working hours, the system will seamlessly shift into motion-activated night light mode.\n"
},
{
"type": "vertical-stack",
"cards": [
{
"type": "markdown",
"content": "### Live Hardware Feed (Outside Door)"
},
{
"type": "tile",
"entity": "light.night_light",
"name": "Door Light Color State",
"state_content": "state",
"state_color": true
},
{
"type": "entities",
"show_header_toggle": false,
"state_color": true,
"entities": [
{
"entity": "binary_sensor.night_light_occupancy",
"name": "Night Light Motion Sensor",
"icon": "mdi:motion-sensor"
}
]
},
{
"type": "custom:mushroom-light-card",
"entity": "light.bedroom_lights",
"use_light_color": true,
"tap_action": {
"action": "none"
},
"hold_action": {
"action": "none"
},
"double_tap_action": {
"action": "none"
}
}
]
}
]
}
]
}
]
}
]
}
}
}
+424
View File
@@ -0,0 +1,424 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace.kobo_dashboard",
"data": {
"config": {
"wallpanel": {
"enabled": true,
"hide_toolbar": false,
"hide_sidebar": true,
"fullscreen": false,
"idle_time": 0
},
"views": [
{
"type": "sections",
"sections": [
{
"type": "grid",
"cards": [
{
"type": "weather-forecast",
"entity": "weather.forecast_home",
"forecast_type": "hourly"
},
{
"type": "vertical-stack",
"cards": [
{
"type": "grid",
"square": false,
"columns": 2,
"cards": [
{
"type": "entity",
"entity": "sensor.living_room_living_room_temperature",
"name": "Living Room Temp"
},
{
"type": "entity",
"entity": "sensor.basement_temperature",
"name": "Basement Temp"
}
]
}
]
},
{
"type": "vertical-stack",
"cards": [
{
"type": "grid",
"square": false,
"columns": 2,
"cards": [
{
"type": "entity",
"entity": "sensor.pool_temperature",
"name": "Pool Temperature"
},
{
"type": "entity",
"entity": "sensor.seatemperatures_sandbanks_today",
"name": "Sandbanks"
}
]
}
]
},
{
"type": "vertical-stack",
"cards": [
{
"type": "grid",
"square": false,
"columns": 2,
"cards": [
{
"type": "entity",
"entity": "input_boolean.in_a_meeting",
"name": "In Meeting",
"icon": "mdi:office-building",
"state_color": true
},
{
"type": "custom:button-card",
"entity": "light.night_light",
"name": "Night Light",
"show_icon": false,
"show_state": false,
"show_label": true,
"label": "[[[\n if (states['light.night_light'].state === 'off') return 'Off';\n const rgb = states['light.night_light'].attributes.rgb_color;\n if (!rgb) return states['light.night_light'].state;\n if (rgb[0] > rgb[1] && rgb[0] > rgb[2]) return 'Red';\n if (rgb[1] > rgb[0] && rgb[1] > rgb[2]) return 'Green';\n return 'Other Color';\n]]]\n",
"styles": {
"label": [
{
"font-weight": "bold"
},
{
"color": "[[[\n if (states['light.night_light'].state === 'off') return '#888888';\n const rgb = states['light.night_light'].attributes.rgb_color;\n if (!rgb) return 'var(--primary-text-color)';\n if (rgb[0] > rgb[1] && rgb[0] > rgb[2]) return '#ff4d4d';\n if (rgb[1] > rgb[0] && rgb[1] > rgb[2]) return '#4dff4d';\n return 'var(--primary-text-color)';\n]]]\n"
}
]
}
}
]
}
]
}
]
},
{
"type": "grid",
"cards": [
{
"entities": [
{
"entity": "calendar.family_calendar",
"show_time": true,
"show_description": true,
"split_multiday_events": false
}
],
"weather": {
"position": "date",
"date": {
"show_conditions": true,
"show_high_temp": true,
"show_low_temp": false,
"show_uv_index": false,
"uv_index_threshold": 0,
"icon_size": "14px",
"font_size": "12px",
"color": "var(--primary-text-color)"
},
"event": {
"show_conditions": true,
"show_temp": true,
"show_uv_index": false,
"uv_index_threshold": 0,
"icon_size": "14px",
"font_size": "12px",
"color": "var(--primary-text-color)"
},
"entity": "weather.forecast_home"
},
"type": "custom:calendar-card-pro"
},
{
"type": "vertical-stack",
"cards": [
{
"type": "gauge",
"entity": "counter.dishwasher_wash_cycles",
"name": "Dishwasher Filter Life",
"min": 0,
"max": 50,
"needle": true,
"severity": {
"green": 0,
"yellow": 10,
"red": 30
}
}
],
"grid_options": {
"columns": 6,
"rows": "auto"
}
},
{
"type": "vertical-stack",
"cards": [
{
"type": "custom:button-card",
"entity": "binary_sensor.150633095332665_door",
"name": "Dishwasher Door",
"show_state": true,
"show_icon": false,
"custom_fields": {
"custom_icon": "[[[\n let iconName = entity.state === 'on' ? 'mdi:door-open' : 'mdi:door-closed';\n let iconColor = entity.state === 'on' ? 'var(--paper-item-icon-active-color, #fdd835)' : 'var(--paper-item-icon-color, #44739e)';\n return `<ha-icon icon=\"${iconName}\" style=\"width: 24px; height: 24px; color: ${iconColor};\"></ha-icon>`;\n]]]\n"
},
"styles": {
"grid": [
{
"grid-template-areas": "\"custom_icon s\" \"n n\""
},
{
"grid-template-columns": "auto 1fr"
},
{
"grid-template-rows": "auto auto"
}
],
"card": [
{
"padding": "12px"
},
{
"border-radius": "12px"
},
{
"height": "82px"
}
],
"custom_fields": {
"custom_icon": [
{
"justify-self": "start"
},
{
"align-self": "center"
},
{
"margin-right": "8px"
},
{
"display": "flex"
}
]
},
"state": [
{
"font-size": "28px"
},
{
"font-weight": "bold"
},
{
"justify-self": "start"
},
{
"align-self": "center"
},
{
"text-transform": "capitalize"
}
],
"name": [
{
"justify-self": "start"
},
{
"font-size": "14px"
},
{
"color": "var(--secondary-text-color)"
},
{
"margin-top": "4px"
}
]
}
},
{
"type": "custom:button-card",
"entity": "sensor.150633095332665_temperature",
"name": "Dishwasher Temp",
"show_state": true,
"show_icon": false,
"custom_fields": {
"custom_icon": "[[[\n let temp = parseFloat(entity.state);\n let iconColor = temp > 40 ? '#ef5350' : 'var(--paper-item-icon-color, #44739e)';\n return `<ha-icon icon=\"mdi:thermometer\" style=\"width: 24px; height: 24px; color: ${iconColor};\"></ha-icon>`;\n]]]\n"
},
"styles": {
"grid": [
{
"grid-template-areas": "\"custom_icon s\" \"n n\""
},
{
"grid-template-columns": "auto 1fr"
},
{
"grid-template-rows": "auto auto"
}
],
"card": [
{
"padding": "12px"
},
{
"border-radius": "12px"
},
{
"height": "82px"
}
],
"custom_fields": {
"custom_icon": [
{
"justify-self": "start"
},
{
"align-self": "center"
},
{
"margin-right": "8px"
},
{
"display": "flex"
}
]
},
"state": [
{
"font-size": "28px"
},
{
"font-weight": "bold"
},
{
"justify-self": "start"
},
{
"align-self": "center"
},
{
"text-transform": "capitalize"
}
],
"name": [
{
"justify-self": "start"
},
{
"font-size": "14px"
},
{
"color": "var(--secondary-text-color)"
},
{
"margin-top": "4px"
}
]
}
}
],
"grid_options": {
"columns": 6,
"rows": "auto"
}
},
{
"type": "vertical-stack",
"cards": [
{
"type": "conditional",
"conditions": [
{
"condition": "state",
"entity": "input_boolean.in_a_meeting",
"state": "off"
}
],
"card": {
"show_name": true,
"show_icon": false,
"type": "button",
"entity": "input_boolean.in_a_meeting",
"name": "✅ AVAILABLE - COME IN",
"show_state": false
}
},
{
"type": "conditional",
"conditions": [
{
"condition": "state",
"entity": "input_boolean.in_a_meeting",
"state": "on"
}
],
"card": {
"show_name": true,
"show_icon": false,
"type": "button",
"entity": "input_boolean.in_a_meeting",
"name": "✋ DO NOT ENTER - IN A MEETING",
"show_state": false
}
}
]
},
{
"type": "vertical-stack",
"cards": [
{
"type": "grid",
"square": false,
"columns": 2,
"cards": [
{
"type": "entity",
"entity": "sensor.google_travel_time_work",
"name": "Work"
},
{
"type": "entity",
"entity": "sensor.google_travel_time_parents",
"name": "Parents"
}
]
}
]
}
]
},
{
"type": "grid",
"cards": [
{
"show_state": false,
"show_name": false,
"camera_view": "auto",
"fit_mode": "cover",
"type": "picture-entity",
"entity": "camera.album_slideshow_google_screensaver",
"camera_image": "camera.album_slideshow_google_screensaver"
}
]
}
]
}
]
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace.map",
"data": {
"config": {
"strategy": {
"type": "map"
}
}
}
}
+577
View File
@@ -0,0 +1,577 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace.morning_hub",
"data": {
"config": {
"title": "Morning Hub",
"views": [
{
"title": "Morning",
"path": "morning",
"icon": "mdi:weather-sunset-up",
"type": "sections",
"max_columns": 3,
"sections": [
{
"cards": [
{
"type": "heading",
"heading": "Overview"
},
{
"type": "custom:clock-weather-card",
"entity": "weather.pirateweather"
},
{
"type": "vertical-stack",
"cards": [
{
"type": "custom:button-card",
"name": "Commute",
"icon": "mdi:car",
"styles": {
"card": [
{
"border-radius": "16px 16px 0px 0px"
},
{
"padding": "12px 16px"
},
{
"border-bottom": "1px solid #f1f2f6"
}
],
"grid": [
{
"grid-template-areas": "\"i n\""
},
{
"grid-template-columns": "30px 1fr"
}
],
"name": [
{
"font-weight": "bold"
},
{
"font-size": "16px"
}
],
"icon": [
{
"color": "#1e90ff"
}
]
}
},
{
"type": "custom:button-card",
"entity": "sensor.google_travel_time_work",
"show_name": true,
"show_label": true,
"name": "Google Maps",
"label": "via Autoroute 13 S",
"styles": {
"card": [
{
"border-radius": "0px"
},
{
"padding": "12px 16px"
},
{
"border-bottom": "1px solid #f1f2f6"
}
],
"grid": [
{
"grid-template-areas": "\"i n stat b\""
},
{
"grid-template-columns": "40px 1fr auto auto"
}
],
"icon": [
{
"color": "#4285F4"
}
],
"name": [
{
"font-weight": "bold"
},
{
"justify-self": "start"
}
],
"label": [
{
"font-size": "12px"
},
{
"color": "gray"
},
{
"justify-self": "start"
}
]
},
"custom_fields": {
"stat": "[[[ return Math.round(parseFloat(entity.state)) + ' min'; ]]]",
"b": "[[[\n var val = parseFloat(entity.state);\n var txt = val < 35 ? 'Good' : (val < 45 ? 'Fair' : 'Heavy');\n var bg = val < 35 ? '#e6f4ea' : (val < 45 ? '#ffe0b2' : '#fce8e6');\n var clr = val < 35 ? '#137333' : (val < 45 ? '#b06000' : '#c5221f');\n return `<span style=\"background-color: ${bg}; color: ${clr}; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: bold; margin-left: 10px;\">${txt}</span>`;\n]]]\n"
}
},
{
"type": "custom:button-card",
"entity": "sensor.waze_to_work_travel_time",
"show_name": true,
"show_label": true,
"name": "Waze",
"label": "via Autoroute 13 S",
"styles": {
"card": [
{
"border-radius": "0px"
},
{
"padding": "12px 16px"
},
{
"border-bottom": "1px solid #f1f2f6"
}
],
"grid": [
{
"grid-template-areas": "\"i n stat b\""
},
{
"grid-template-columns": "40px 1fr auto auto"
}
],
"icon": [
{
"color": "#33ccff"
}
],
"name": [
{
"font-weight": "bold"
},
{
"justify-self": "start"
}
],
"label": [
{
"font-size": "12px"
},
{
"color": "gray"
},
{
"justify-self": "start"
}
]
},
"custom_fields": {
"stat": "[[[ return Math.round(parseFloat(entity.state)) + ' min'; ]]]",
"b": "[[[\n var val = parseFloat(entity.state);\n var txt = val < 35 ? 'Good' : (val < 45 ? 'Fair' : 'Heavy');\n var bg = val < 35 ? '#e6f4ea' : (val < 45 ? '#ffe0b2' : '#fce8e6');\n var clr = val < 35 ? '#137333' : (val < 45 ? '#b06000' : '#c5221f');\n return `<span style=\"background-color: ${bg}; color: ${clr}; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: bold; margin-left: 10px;\">${txt}</span>`;\n]]]\n"
}
},
{
"type": "custom:button-card",
"entity": "sensor.google_travel_time_work",
"show_name": true,
"show_label": true,
"name": "[[[ \n var mins = Math.round(parseFloat(entity.state));\n var status = mins < 35 ? 'Traffic is flowing normally' : (mins < 45 ? 'Minor traffic delays' : 'Heavy traffic alerts');\n return `${status} • ${mins} min`;\n]]]\n",
"label": "Typical commute: ~25 min",
"icon": "[[[ return parseFloat(entity.state) < 35 ? 'mdi:check-circle' : (parseFloat(entity.state) < 45 ? 'mdi:alert-circle' : 'mdi:alert-octagon'); ]]]",
"styles": {
"card": [
{
"border-radius": "0px 0px 16px 16px"
},
{
"padding": "12px 16px"
}
],
"grid": [
{
"grid-template-areas": "\"i n\""
},
{
"grid-template-columns": "40px 1fr"
}
],
"icon": [
{
"color": "[[[ return parseFloat(entity.state) < 35 ? '#2ecc71' : (parseFloat(entity.state) < 45 ? '#e67e22' : '#e74c3c'); ]]]"
}
],
"name": [
{
"font-weight": "500"
},
{
"justify-self": "start"
}
],
"label": [
{
"font-size": "12px"
},
{
"color": "gray"
},
{
"justify-self": "start"
}
]
}
}
]
},
{
"type": "heading",
"heading": "Control"
},
{
"type": "vertical-stack",
"cards": [
{
"type": "custom:button-card",
"name": "Quick Actions",
"icon": "mdi:rocket-launch",
"styles": {
"card": [
{
"background": "none"
},
{
"box-shadow": "none"
},
{
"padding": "4px 0px"
}
],
"grid": [
{
"grid-template-areas": "\"i n\""
},
{
"grid-template-columns": "30px 1fr"
}
],
"name": [
{
"font-weight": "bold"
},
{
"font-size": "16px"
}
],
"icon": [
{
"color": "#9b59b6"
}
]
}
},
{
"type": "grid",
"square": false,
"columns": 1,
"cards": [
{
"type": "custom:button-card",
"name": "Open Waze",
"icon": "mdi:waze",
"tap_action": {
"action": "url",
"url_path": "https://waze.com/ul"
},
"styles": {
"card": [
{
"background-color": "#3498db"
},
{
"border-radius": "16px"
},
{
"color": "white"
},
{
"padding": "18px 16px"
}
],
"grid": [
{
"grid-template-areas": "\"i n\""
},
{
"grid-template-columns": "40px 1fr"
},
{
"justify-items": "start"
}
],
"name": [
{
"font-weight": "bold"
},
{
"font-size": "16px"
}
],
"icon": [
{
"width": "26px"
}
]
}
},
{
"type": "custom:button-card",
"name": "Open Google Maps",
"icon": "mdi:google-maps",
"tap_action": {
"action": "url",
"url_path": "https://maps.google.com"
},
"styles": {
"card": [
{
"background-color": "#2ecc71"
},
{
"border-radius": "16px"
},
{
"color": "white"
},
{
"padding": "18px 16px"
}
],
"grid": [
{
"grid-template-areas": "\"i n\""
},
{
"grid-template-columns": "40px 1fr"
},
{
"justify-items": "start"
}
],
"name": [
{
"font-weight": "bold"
},
{
"font-size": "16px"
}
],
"icon": [
{
"width": "26px"
}
]
}
}
]
}
]
}
]
},
{
"cards": [
{
"type": "heading",
"heading": "Schedule"
},
{
"title": "Today's Calendar",
"entities": [
"calendar.family_calendar"
],
"default_view": "agenda",
"first_day_of_week": 0,
"week_days": [
0,
1,
2,
3,
4,
5,
6
],
"week_start_hour": 0,
"week_end_hour": 23,
"lock_schedule_hours": false,
"hide_the_past": false,
"past_event_mode": "none",
"disable_swipe_controls": false,
"show_all_events_month": false,
"show_all_details_month": false,
"hide_empty_days": true,
"agenda_compact_events": false,
"shorten_event_times": false,
"time_zone": "",
"display_full_weekday_names": false,
"compact_width": false,
"show_current_time_bar": false,
"show_event_location": false,
"use_short_location": false,
"event_location_font_size": 9,
"background_opacity": 0,
"header_background_opacity": 0,
"event_calendar_friendly_name": false,
"event_title_prefix": "none",
"combine_style": "bars",
"combine_background": "primary",
"event_color_mode": "classic",
"event_neutral_background": "#F8F3E9",
"event_tint_opacity": 80,
"event_color_bar_width": 18,
"day_badges": [],
"day_badge_layout_week": "inline",
"hide_calendars": true,
"hide_header": true,
"hide_year": false,
"hide_controls": false,
"hide_navigation_buttons": false,
"hide_add_event_button": false,
"hide_view_selector": false,
"hide_dark_mode_toggle": false,
"show_dashboard_nav_button": false,
"header_dashboard_path": null,
"header_weather_sensor": "",
"calendar_person_entities": {},
"default_hidden_calendars": [],
"color_scheme": "auto",
"enable_event_management": true,
"event_modal_size": "medium",
"type": "custom:daylight-calendar-card",
"rolling_days": 7,
"compact_height": true,
"card_mod": {
"style": "ha-card {\n border-radius: 18px !important;\n padding: 16px !important;\n background-color: transparent !important;\n box-shadow: none !important;\n border: none !important;\n}\n.daylight-header-title {\n font-weight: bold !important;\n font-size: 18px !important;\n color: var(--primary-text-color) !important;\n}\n.daylight-event-wrapper {\n background: #ffffff !important;\n border: 1px solid #e1e4e8 !important;\n border-radius: 14px !important;\n margin-bottom: 12px !important;\n padding: 14px 16px !important;\n box-shadow: 0 4px 12px rgba(0,0,0,0.02) !important;\n border-left: 5px solid var(--daylight-event-color, #2196f3) !important;\n}\n.daylight-event-title {\n font-weight: bold !important;\n font-size: 15px !important;\n color: #2c3e50 !important;\n}\n.daylight-event-time {\n font-size: 12px !important;\n color: #95a5a6 !important;\n margin-bottom: 2px !important;\n}\n"
},
"hide_calendar_names": false,
"grid_options": {
"rows": 5,
"columns": 12
}
},
{
"type": "statistics-graph",
"entities": [
"sensor.google_travel_time_work"
],
"days_to_show": 1,
"period": "hour",
"chart_type": "line",
"stat_types": [
"mean"
]
},
{
"type": "statistics-graph",
"entities": [
"sensor.waze_to_work_travel_time_waze_to_parents"
],
"days_to_show": 1,
"period": "hour",
"chart_type": "line",
"stat_types": [
"mean"
]
}
]
},
{
"type": "grid",
"cards": [
{
"type": "custom:button-card",
"name": "[[[\n const hour = new Date().getHours();\n if (hour >= 5 && hour < 12) {\n return \"Good Morning, Franco!\";\n } else if (hour >= 12 && hour < 17) {\n return \"Good Afternoon, Franco!\";\n } else {\n return \"Good Evening, Franco!\";\n }\n]]]",
"label": "[[[ const lnow = new Date(); return lnow.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }); ]]]",
"show_label": true,
"styles": {
"card": [
{
"background": "[[[\n const hour = new Date().getHours();\n \n if (hour >= 5 && hour < 11) {\n // Sunrise: Soft orange to warm pink dawn gradient\n return 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%, #feada6 100%)';\n } else if (hour >= 11 && hour < 16) {\n // Mid-day: Bright, vibrant clear sky blue gradient\n return 'linear-gradient(135deg, #2980b9 0%, #6dd5fa 50%, #ffffff 100%)';\n } else if (hour >= 16 && hour < 21) {\n // Sunset: Deep twilight purple to vibrant orange gradient\n return 'linear-gradient(135deg, #111827 0%, #701a75 40%, #f59e0b 100%)';\n } else {\n // Night: Deep cosmic midnight blue and space gray gradient\n return 'linear-gradient(135deg, #0f172a 0%, #1e293b 60%, #334155 100%)';\n }\n]]]"
},
{
"background-size": "cover"
},
{
"background-position": "center"
},
{
"height": "110px"
},
{
"border-radius": "18px"
},
{
"padding": "16px"
}
],
"name": [
{
"justify-self": "start"
},
{
"font-weight": "bold"
},
{
"font-size": "22px"
},
{
"color": "white"
},
{
"text-shadow": "0px 2px 4px rgba(0,0,0,0.2)"
}
],
"label": [
{
"justify-self": "start"
},
{
"font-size": "14px"
},
{
"color": "white"
},
{
"opacity": "0.9"
},
{
"text-shadow": "0px 1px 3px rgba(0,0,0,0.2)"
}
]
}
},
{
"show_state": false,
"show_name": false,
"camera_view": "auto",
"fit_mode": "cover",
"type": "picture-entity",
"entity": "camera.album_slideshow_google_screensaver",
"camera_image": "camera.album_slideshow_google_screensaver"
}
]
}
]
}
]
}
}
}
+345
View File
@@ -0,0 +1,345 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace.morning_ipad",
"data": {
"config": {
"wallpanel": {
"enabled": true,
"hide_toolbar": false,
"hide_sidebar": true,
"fullscreen": false,
"idle_time": 0
},
"title": "Morning Hub",
"views": [
{
"title": "Morning",
"path": "morning",
"icon": "mdi:weather-sunset-up",
"type": "sections",
"max_columns": 3,
"sections": [
{
"cards": [
{
"type": "heading",
"heading": "Overview"
},
{
"type": "custom:clock-weather-card",
"entity": "weather.pirateweather"
},
{
"type": "vertical-stack",
"cards": [
{
"type": "custom:button-card",
"name": "Commute",
"icon": "mdi:car",
"styles": {
"card": [
{
"border-radius": "16px 16px 0px 0px"
},
{
"padding": "12px 16px"
},
{
"border-bottom": "1px solid #f1f2f6"
}
],
"grid": [
{
"grid-template-areas": "\"i n\""
},
{
"grid-template-columns": "30px 1fr"
}
],
"name": [
{
"font-weight": "bold"
},
{
"font-size": "16px"
}
],
"icon": [
{
"color": "#1e90ff"
}
]
}
},
{
"type": "custom:button-card",
"entity": "sensor.google_travel_time_work",
"show_name": true,
"show_label": true,
"name": "Google Maps",
"label": "via Autoroute 13 S",
"styles": {
"card": [
{
"border-radius": "0px"
},
{
"padding": "12px 16px"
},
{
"border-bottom": "1px solid #f1f2f6"
}
],
"grid": [
{
"grid-template-areas": "\"i n stat b\""
},
{
"grid-template-columns": "40px 1fr auto auto"
}
],
"icon": [
{
"color": "#4285F4"
}
],
"name": [
{
"font-weight": "bold"
},
{
"justify-self": "start"
}
],
"label": [
{
"font-size": "12px"
},
{
"color": "gray"
},
{
"justify-self": "start"
}
]
},
"custom_fields": {
"stat": "[[[ return Math.round(parseFloat(entity.state)) + ' min'; ]]]",
"b": "[[[\n var val = parseFloat(entity.state);\n var txt = val < 35 ? 'Good' : (val < 45 ? 'Fair' : 'Heavy');\n var bg = val < 35 ? '#e6f4ea' : (val < 45 ? '#ffe0b2' : '#fce8e6');\n var clr = val < 35 ? '#137333' : (val < 45 ? '#b06000' : '#c5221f');\n return `<span style=\"background-color: ${bg}; color: ${clr}; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: bold; margin-left: 10px;\">${txt}</span>`;\n]]]\n"
}
},
{
"type": "custom:button-card",
"entity": "sensor.waze_to_work_travel_time",
"show_name": true,
"show_label": true,
"name": "Waze",
"label": "via Autoroute 13 S",
"styles": {
"card": [
{
"border-radius": "0px"
},
{
"padding": "12px 16px"
},
{
"border-bottom": "1px solid #f1f2f6"
}
],
"grid": [
{
"grid-template-areas": "\"i n stat b\""
},
{
"grid-template-columns": "40px 1fr auto auto"
}
],
"icon": [
{
"color": "#33ccff"
}
],
"name": [
{
"font-weight": "bold"
},
{
"justify-self": "start"
}
],
"label": [
{
"font-size": "12px"
},
{
"color": "gray"
},
{
"justify-self": "start"
}
]
},
"custom_fields": {
"stat": "[[[ return Math.round(parseFloat(entity.state)) + ' min'; ]]]",
"b": "[[[\n var val = parseFloat(entity.state);\n var txt = val < 35 ? 'Good' : (val < 45 ? 'Fair' : 'Heavy');\n var bg = val < 35 ? '#e6f4ea' : (val < 45 ? '#ffe0b2' : '#fce8e6');\n var clr = val < 35 ? '#137333' : (val < 45 ? '#b06000' : '#c5221f');\n return `<span style=\"background-color: ${bg}; color: ${clr}; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: bold; margin-left: 10px;\">${txt}</span>`;\n]]]\n"
}
},
{
"type": "custom:button-card",
"entity": "sensor.google_travel_time_work",
"show_name": true,
"show_label": true,
"name": "[[[ \n var mins = Math.round(parseFloat(entity.state));\n var status = mins < 35 ? 'Traffic normally' : (mins < 45 ? 'Minor traffic delays' : 'Heavy traffic alerts');\n return `${status} • ${mins} min`;\n]]]\n",
"label": "Typical commute: ~25 min",
"icon": "[[[ return parseFloat(entity.state) < 35 ? 'mdi:check-circle' : (parseFloat(entity.state) < 45 ? 'mdi:alert-circle' : 'mdi:alert-octagon'); ]]]",
"styles": {
"card": [
{
"border-radius": "0px 0px 16px 16px"
},
{
"padding": "12px 16px"
}
],
"grid": [
{
"grid-template-areas": "\"i n\""
},
{
"grid-template-columns": "40px 1fr"
}
],
"icon": [
{
"color": "[[[ return parseFloat(entity.state) < 35 ? '#2ecc71' : (parseFloat(entity.state) < 45 ? '#e67e22' : '#e74c3c'); ]]]"
}
],
"name": [
{
"font-weight": "500"
},
{
"justify-self": "start"
}
],
"label": [
{
"font-size": "12px"
},
{
"color": "gray"
},
{
"justify-self": "start"
}
]
}
}
]
},
{
"type": "heading",
"heading": "Control"
}
]
},
{
"cards": [
{
"type": "heading",
"heading": "Schedule"
},
{
"title": "Today's Calendar",
"entities": [
"calendar.family_calendar"
],
"default_view": "agenda",
"first_day_of_week": 0,
"week_days": [
0,
1,
2,
3,
4,
5,
6
],
"week_start_hour": 0,
"week_end_hour": 23,
"lock_schedule_hours": false,
"hide_the_past": false,
"past_event_mode": "none",
"disable_swipe_controls": false,
"show_all_events_month": false,
"show_all_details_month": false,
"hide_empty_days": true,
"agenda_compact_events": false,
"shorten_event_times": false,
"time_zone": "",
"display_full_weekday_names": false,
"compact_width": false,
"show_current_time_bar": false,
"show_event_location": false,
"use_short_location": false,
"event_location_font_size": 9,
"background_opacity": 0,
"header_background_opacity": 0,
"event_calendar_friendly_name": false,
"event_title_prefix": "none",
"combine_style": "bars",
"combine_background": "primary",
"event_color_mode": "classic",
"event_neutral_background": "#F8F3E9",
"event_tint_opacity": 80,
"event_color_bar_width": 18,
"day_badges": [],
"day_badge_layout_week": "inline",
"hide_calendars": true,
"hide_header": true,
"hide_year": false,
"hide_controls": false,
"hide_navigation_buttons": false,
"hide_add_event_button": false,
"hide_view_selector": false,
"hide_dark_mode_toggle": false,
"show_dashboard_nav_button": false,
"header_dashboard_path": null,
"header_weather_sensor": "",
"calendar_person_entities": {},
"default_hidden_calendars": [],
"color_scheme": "auto",
"enable_event_management": true,
"event_modal_size": "medium",
"type": "custom:daylight-calendar-card",
"rolling_days": 7,
"compact_height": true,
"card_mod": {
"style": "ha-card {\n border-radius: 18px !important;\n padding: 16px !important;\n background-color: transparent !important;\n box-shadow: none !important;\n border: none !important;\n}\n.daylight-header-title {\n font-weight: bold !important;\n font-size: 18px !important;\n color: var(--primary-text-color) !important;\n}\n.daylight-event-wrapper {\n background: #ffffff !important;\n border: 1px solid #e1e4e8 !important;\n border-radius: 14px !important;\n margin-bottom: 12px !important;\n padding: 14px 16px !important;\n box-shadow: 0 4px 12px rgba(0,0,0,0.02) !important;\n border-left: 5px solid var(--daylight-event-color, #2196f3) !important;\n}\n.daylight-event-title {\n font-weight: bold !important;\n font-size: 15px !important;\n color: #2c3e50 !important;\n}\n.daylight-event-time {\n font-size: 12px !important;\n color: #95a5a6 !important;\n margin-bottom: 2px !important;\n}\n"
},
"hide_calendar_names": false
}
]
},
{
"type": "grid",
"cards": [
{
"show_state": false,
"show_name": false,
"camera_view": "auto",
"fit_mode": "cover",
"type": "picture-entity",
"entity": "camera.album_slideshow_google_screensaver",
"camera_image": "camera.album_slideshow_google_screensaver"
}
]
}
]
}
]
}
}
}
+141
View File
@@ -0,0 +1,141 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace.network_cellphone",
"data": {
"config": {
"views": [
{
"type": "sections",
"sections": [
{
"type": "grid",
"cards": [
{
"type": "heading",
"icon": "mdi:fridge",
"heading": "Network Status",
"heading_style": "title"
},
{
"type": "vertical-stack",
"cards": [
{
"type": "entities",
"show_header_toggle": false,
"state_color": true,
"entities": [
{
"entity": "sensor.gt_ax11000_connected_devices",
"name": "Active Clients",
"icon": "mdi:laptop"
},
{
"entity": "sensor.gt_ax11000_wan_download_speed",
"name": "Internet Download",
"icon": "mdi:arrow-down"
},
{
"entity": "sensor.gt_ax11000_wan_upload_speed",
"name": "Internet Upload",
"icon": "mdi:arrow-up"
},
{
"entity": "sensor.gt_ax11000_wired_download_speed",
"name": "Wired D/L",
"icon": "mdi:lan-connect"
},
{
"entity": "sensor.gt_ax11000_wired_upload_speed",
"name": "Wired U/L",
"icon": "mdi:lan-pending"
}
],
"card_mod": {
"style": "ha-card {\n background: #0d0e12 !important;\n border-left: 1px solid #1a1d26 !important;\n border-right: 1px solid #1a1d26 !important;\n border-top: none !important;\n border-bottom: none !important;\n border-radius: 0px !important;\n box-shadow: none !important;\n --primary-text-color: #f1f5f9;\n --secondary-text-color: #94a3b8;\n --paper-item-icon-color: #3b82f6;\n}\n.card-content {\n padding: 16px 20px 4px 20px !important;\n}\n"
}
}
]
},
{
"type": "custom:layout-card",
"layout_type": "grid",
"grid_options": {
"columns": "full",
"rows": 3
},
"layout": {
"grid-template-columns": "3fr 1fr",
"grid-gap": "12px"
},
"cards": [
{
"type": "custom:mini-graph-card",
"entities": [
{
"entity": "sensor.gt_ax11000_wan_download_speed",
"name": "Download",
"color": null
},
{
"entity": "sensor.gt_ax11000_wan_upload_speed",
"name": "Upload",
"color": null
}
],
"hours_to_show": 6,
"points_per_hour": 6,
"line_width": 4,
"show": {
"name": false,
"icon": false,
"state": true,
"legend": true,
"labels": false
},
"card_mod": {
"style": "ha-card {\n background: #0d0e12 !important;\n border: 1px solid #1a1d26 !important;\n border-radius: 12px !important;\n padding: 12px !important;\n height: 100% !important;\n box-sizing: border-box;\n}\n.header {\n padding: 0 !important;\n}\n.states {\n font-size: 14px !important;\n font-weight: 600;\n}\n.states--secondary {\n font-size: 12px !important;\n}\n"
}
},
{
"type": "vertical-stack",
"cards": [
{
"type": "sensor",
"entity": "sensor.gt_ax11000_temperature_cpu",
"name": "CPU Temp",
"graph": "none",
"card_mod": {
"style": "ha-card {\n background: #0d0e12 !important;\n border: 1px solid #1a1d26 !important;\n border-radius: 12px !important;\n padding: 8px 12px !important;\n box-sizing: border-box;\n margin-bottom: 4px !important;\n}\n.name {\n font-size: 12px !important;\n color: #808080 !important;\n}\n.value {\n font-size: 16px !important;\n font-weight: 600 !important;\n}\n"
}
}
]
}
]
},
{
"type": "gauge",
"entity": "sensor.gt_ax11000_cpu",
"grid_options": {
"columns": 6,
"rows": "auto"
},
"name": "CPU Usage"
},
{
"type": "gauge",
"entity": "sensor.gt_ax11000_ram",
"grid_options": {
"columns": 6,
"rows": "auto"
},
"name": "RAM Ussage"
}
]
}
]
}
]
}
}
}
+90
View File
@@ -0,0 +1,90 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace_dashboards",
"data": {
"items": [
{
"id": "map",
"icon": "mdi:map",
"title": "Map",
"url_path": "map",
"require_admin": false,
"show_in_sidebar": false,
"mode": "storage"
},
{
"id": "dashboard_home",
"show_in_sidebar": true,
"title": "Home",
"require_admin": false,
"mode": "storage",
"url_path": "dashboard-home"
},
{
"id": "alarm_section",
"show_in_sidebar": true,
"title": "Alarm Section",
"require_admin": false,
"mode": "storage",
"url_path": "alarm-section"
},
{
"id": "dashboard_temp",
"show_in_sidebar": true,
"title": "Stats",
"require_admin": false,
"mode": "storage",
"url_path": "dashboard-temp"
},
{
"id": "kobo_dashboard",
"show_in_sidebar": false,
"title": "Kobo Dashboard",
"require_admin": false,
"mode": "storage",
"url_path": "kobo-dashboard"
},
{
"id": "morning_hub",
"show_in_sidebar": true,
"title": "Morning Hub",
"require_admin": false,
"mode": "storage",
"url_path": "morning-hub"
},
{
"id": "morning_ipad",
"show_in_sidebar": false,
"title": "Morning iPad",
"require_admin": false,
"mode": "storage",
"url_path": "morning-ipad"
},
{
"id": "network_cellphone",
"show_in_sidebar": true,
"title": "Network CellPhone",
"require_admin": false,
"mode": "storage",
"url_path": "network-cellphone"
},
{
"id": "in_meeting",
"show_in_sidebar": true,
"title": "In Meeting",
"require_admin": false,
"mode": "storage",
"url_path": "in-meeting"
},
{
"id": "dashboard_music",
"show_in_sidebar": true,
"title": "Music",
"require_admin": false,
"mode": "storage",
"url_path": "dashboard-music"
}
]
}
}
+304
View File
@@ -0,0 +1,304 @@
{
"version": 1,
"minor_version": 1,
"key": "lovelace_resources",
"data": {
"items": [
{
"id": "cc55565add58413bbcadba9f9cf8caba",
"url": "/hacsfiles/lovelace-mushroom/mushroom.js?hacstag=444350375511",
"type": "module"
},
{
"id": "387073c1f6fe4a658c6ca7e185b1134c",
"url": "/hacsfiles/sunsynk-power-flow-card/sunsynk-power-flow-card.js?hacstag=613588535733",
"type": "module"
},
{
"id": "52ed5bcf36d24b36a1f1b3584ef7b64c",
"url": "/hacsfiles/power-flow-card-plus/power-flow-card-plus.js?hacstag=618081815037",
"type": "module"
},
{
"id": "6bde3dd4cdd0478d9e77c0d68d093cfd",
"url": "/hacsfiles/energy-sankey/energy-sankey.js?hacstag=875798863104",
"type": "module"
},
{
"id": "ee7be1b18ae84060a5d515640f6e5739",
"url": "/seatemperatures_frontend/sea-temperatures-card.js?v=3.1.0",
"type": "module"
},
{
"id": "53837b51a371459b95ffc0989ce615fb",
"url": "/hacsfiles/calendar-card-pro/calendar-card-pro.js?hacstag=939311749320",
"type": "module"
},
{
"id": "c86cd6c3d8eb451ebde1a41ad5a1dd33",
"url": "/hacsfiles/lovelace-auto-entities/auto-entities.js?hacstag=1677445841161",
"type": "module"
},
{
"id": "0b28e2d01d0d4033b2f0a339cc7d86c1",
"url": "/ha_washdata/ha-washdata-card.js?v=1781361396",
"type": "module"
},
{
"id": "8ce0dbd60e7641be90f2e08594d1db2e",
"url": "/hacsfiles/HA-Firemote/HA-Firemote.js?hacstag=536329656419",
"type": "module"
},
{
"id": "6ad5d0d7a4c9435a85476e49d22d4df4",
"url": "/hacsfiles/versatile-thermostat-ui-card/versatile-thermostat-ui-card.js?hacstag=714354847320",
"type": "module"
},
{
"id": "0476eb038c9a47a7b654a078b824a9d1",
"url": "/hacsfiles/scheduler-card/scheduler-card.js?hacstag=2862701574018",
"type": "module"
},
{
"id": "5182b0b7bbc94cd7956a7eaf1389ebd1",
"url": "/hacsfiles/lovelace-thermostat-pro-timeline/thermostat-pro-timeline.js?hacstag=1072526491300",
"type": "module"
},
{
"id": "193c54e0fb0947dabbae7a889f0b5f46",
"url": "/local/thermostat-pro-timeline.js?v=56675817588421",
"type": "module"
},
{
"id": "5447aa1e0b934b57b58b98751c3c3350",
"url": "/hacsfiles/homie-dashboard/homie-dashboard.js?hacstag=1221720415400",
"type": "module"
},
{
"id": "9a535448bf5645889b43fbdf481bb225",
"url": " /local/community/homie-dashboard/homie-dashboard.js",
"type": "module"
},
{
"id": "3996b960ca1a49cd81de517c5e1b658a",
"url": "/hacsfiles/sensor-bar-card-plus/sensor-bar-card-plus.js?hacstag=1183027678163",
"type": "module"
},
{
"id": "88b90f2b1d6b45f0b44bee786ee1a525",
"url": "/taskmate/taskmate-attr-resolver.js?v=5.0.1",
"type": "module"
},
{
"id": "0796c92931dd458e81dc9c2677248274",
"url": "/taskmate/taskmate-localize.js?v=5.0.1",
"type": "module"
},
{
"id": "d2e281920ac7409583eafff1f1db69bd",
"url": "/taskmate/taskmate-design.js?v=5.0.1",
"type": "module"
},
{
"id": "9eae9632f0734480be5edd784b030673",
"url": "/taskmate/taskmate-badges-card.js?v=5.0.1",
"type": "module"
},
{
"id": "5df49cd8a4eb420497b5df22cb31362c",
"url": "/taskmate/taskmate-child-card.js?v=5.0.1",
"type": "module"
},
{
"id": "b44c87d0d74f468cb96c5eabc340f9ee",
"url": "/taskmate/taskmate-rewards-card.js?v=5.0.1",
"type": "module"
},
{
"id": "787a96f9c30548708165a48457c99feb",
"url": "/taskmate/taskmate-approvals-card.js?v=5.0.1",
"type": "module"
},
{
"id": "84031597de424ad68e7c2bc66d06b72e",
"url": "/taskmate/taskmate-points-card.js?v=5.0.1",
"type": "module"
},
{
"id": "02613225844746b699bfbfac9642eca2",
"url": "/taskmate/taskmate-reorder-card.js?v=5.0.1",
"type": "module"
},
{
"id": "7a06a07e17ba4b31b712bc91b77360b4",
"url": "/taskmate/taskmate-overview-card.js?v=5.0.1",
"type": "module"
},
{
"id": "08b42b53b33b4fa88db932455cf8d5aa",
"url": "/taskmate/taskmate-activity-card.js?v=5.0.1",
"type": "module"
},
{
"id": "15907eff51784acdba88ff694b574509",
"url": "/taskmate/taskmate-streak-card.js?v=5.0.1",
"type": "module"
},
{
"id": "1bd6bb5c0a6d4f0d916e3d82cb7fac39",
"url": "/taskmate/taskmate-weekly-card.js?v=5.0.1",
"type": "module"
},
{
"id": "289ce5943f5a4972a593697db4c96ece",
"url": "/taskmate/taskmate-graph-card.js?v=5.0.1",
"type": "module"
},
{
"id": "d2904acee4c74a398dcc7b996c23e7e7",
"url": "/taskmate/taskmate-reward-progress-card.js?v=5.0.1",
"type": "module"
},
{
"id": "46d378736fd54a26be253f2dfb27202e",
"url": "/taskmate/taskmate-leaderboard-card.js?v=5.0.1",
"type": "module"
},
{
"id": "db11ae36457842fa826c7991e1d9c6f4",
"url": "/taskmate/taskmate-parent-dashboard-card.js?v=5.0.1",
"type": "module"
},
{
"id": "ce85f301b59a412e9abfe5ffd51cb9a2",
"url": "/taskmate/taskmate-penalties-card.js?v=5.0.1",
"type": "module"
},
{
"id": "1959e1629de2422daa8adaf87069c288",
"url": "/taskmate/taskmate-bonuses-card.js?v=5.0.1",
"type": "module"
},
{
"id": "2548524b269c4959bc7e0e967df943af",
"url": "/taskmate/taskmate-points-display-card.js?v=5.0.1",
"type": "module"
},
{
"id": "36ada49368db4187a283fbbbef45454f",
"url": "/taskmate/taskmate-calendar-card.js?v=5.0.1",
"type": "module"
},
{
"id": "67c32687acd2497b90030ef141f4b6db",
"url": "/taskmate/taskmate-photo-gallery-card.js?v=5.0.1",
"type": "module"
},
{
"id": "6f0c8348a91f4b74a9b846ab3f62621f",
"url": "/taskmate/taskmate-family-goal-card.js?v=5.0.1",
"type": "module"
},
{
"id": "e92ef0caff41454e9f49ea966ed99e41",
"url": "/hacsfiles/lovelace-multiple-entity-row/multiple-entity-row.js?hacstag=178921037471",
"type": "module"
},
{
"id": "4d1a17aa96b74064af713fc5e9334862",
"url": "/hacsfiles/ha-treemap-card/treemap-card.js?hacstag=11141174680152",
"type": "module"
},
{
"id": "d3c3e144a91247ce8ac524814737d7f8",
"url": "/hacsfiles/lovelace-wallpanel/wallpanel.js?hacstag=3095064164651",
"type": "module"
},
{
"id": "0922e4ba1c7c4f11969d0257689af5a3",
"url": "/hacsfiles/button-card/button-card.js?hacstag=146194325701",
"type": "module"
},
{
"id": "e9bc54c181334248b39e0f8874998041",
"url": "/hacsfiles/atomic-calendar-revive/atomic-calendar-revive.js?hacstag=2465497471031",
"type": "module"
},
{
"id": "c9b6518d876d437a9671194eb1919026",
"url": "/hacsfiles/daylight-calendar-card/skylight-calendar-card.js?hacstag=1143954763470",
"type": "module"
},
{
"id": "5612169a8e664ae5bef0aa3803a076db",
"url": "/hacsfiles/platinum-weather-card/platinum-weather-card.js?hacstag=488086721105",
"type": "module"
},
{
"id": "449ff43140654e4bbe2c7b2d361dfacc",
"url": "/hacsfiles/clock-weather-card/clock-weather-card.js?hacstag=522634019294",
"type": "module"
},
{
"id": "dab9cfff6e834886bcf4d5c7c9968c9e",
"url": "/webrtc/webrtc-camera.js?v=v3.6.1",
"type": "module"
},
{
"id": "94255369635043dfbbf598581e56569d",
"url": "/hacsfiles/apexcharts-card/apexcharts-card.js?hacstag=331701152223",
"type": "module"
},
{
"id": "1251007d3e5a44a7bbced052f41b5f9c",
"url": "/hacsfiles/mini-graph-card/mini-graph-card-bundle.js?hacstag=1512800620130",
"type": "module"
},
{
"id": "0893e5cfeaf1486f9c44cef25048230a",
"url": "/hacsfiles/lovelace-layout-card/layout-card.js?hacstag=156434866247",
"type": "module"
},
{
"id": "8713ea9c008e4270bb28085eed5be959",
"url": "/hacsfiles/Statistics-Graph-Chart-Card/statistics-graph-chart-card.js?hacstag=1034932856330",
"type": "module"
},
{
"id": "6c3f7d87683d4473b5dc8fe8177535f6",
"url": "/hacsfiles/simple-thermostat/simple-thermostat.js?hacstag=1230152807410",
"type": "module"
},
{
"id": "9a17b155e3fb42949f2b2f64e24c42d5",
"url": "/hacsfiles/mass-player-card/mass-player-card.js?hacstag=1043277514280",
"type": "module"
},
{
"id": "e635fcf579d24ceb97758cc125ed5664",
"url": "/my_music_library/my-music-library-card.js?v=3.10.4",
"type": "module"
},
{
"id": "15981f60b53f4d5886272ea59de3861b",
"url": "/hacsfiles/uptime-card/uptime-card.js?hacstag=3505098670160",
"type": "module"
},
{
"id": "a3fc4486bc2c4edab1f38af295b4ef3f",
"url": "/album_slideshow_static/album-slideshow-card.js?v=1.5.0",
"type": "module"
},
{
"id": "73dbd2de03de4530b451a55bfe6c46cf",
"url": "/taskmate/taskmate-routine-card.js?v=5.0.1",
"type": "module"
},
{
"id": "a2f2c6ca7b354458a8d4910d453f0684",
"url": "/climate_scheduler/static/climate-scheduler-card.js?v=1.15.1",
"type": "module"
}
]
}
}
-807
View File
@@ -1,807 +0,0 @@
# Home Assistant OpenCode Rules
You are working directly within a Home Assistant installation. Your working directory is `/homeassistant`, which is the live Home Assistant configuration directory.
## CRITICAL: User Consent and Scope Rules
You MUST follow these rules strictly:
1. **Never exceed the user's request** - Do exactly what the user asks, nothing more. Do not "improve" or "enhance" beyond the stated scope.
2. **Never make changes without explicit approval** - Before modifying ANY file:
- Show the user exactly what you plan to change
- Wait for their explicit confirmation ("yes", "go ahead", "do it", etc.)
- If they haven't approved, DO NOT proceed
3. **Ask, don't assume** - If the user's request is ambiguous:
- Ask clarifying questions first
- Present options and let them choose
- Never guess at their intent
4. **Read-only by default** - When investigating or troubleshooting:
- Only read files and gather information
- Present findings and recommendations
- Wait for user instruction before making any changes
5. **One change at a time** - When making approved changes:
- Make the minimum change needed
- Show what was changed
- Let the user verify before proceeding to any next step
6. **No unsolicited modifications** - Never:
- "Clean up" code the user didn't ask about
- Add features they didn't request
- Refactor working configurations
- Fix issues they haven't mentioned
7. **Respect "no"** - If a user declines a suggestion, do not:
- Repeat the suggestion
- Make the change anyway
- Try to convince them otherwise
## Environment Context
- You are running inside the OpenCode app
- The current directory (`/homeassistant`) contains the live Home Assistant configuration
- Changes to YAML files here directly affect the Home Assistant instance
- If add-on folder access is enabled, `/addons` and `/addon_configs` are available for Home Assistant add-on development. Treat `/addon_configs` as sensitive and only inspect or modify these folders when the user explicitly asks.
- You may have access to MCP tools for interacting with Home Assistant (check with the user)
## Home Assistant Interaction Model
There are three primary, safe ways to interact with Home Assistant:
### 1. Configuration Files (YAML)
The standard way to define and customize Home Assistant behavior:
- Automations, scripts, scenes, and blueprints
- Integration and sensor configurations
- Templates, packages, and customizations
- Dashboard (Lovelace) definitions
These files are designed for user editing and are the source of truth for your Home Assistant setup.
### 2. MCP Tools (Runtime API)
Real-time interaction with the running Home Assistant instance:
- Query current entity states and history
- Control devices and call services
- Validate configurations
- Diagnose issues and detect anomalies
- Report OpenCode/HA agent capability status with `get_agent_capabilities`
### Native Home Assistant LLM Platform
Home Assistant is developing a native `llm` integration where Core integrations and custom integrations can expose curated tools through `<integration>/llm.py` and registered LLM APIs. New Home Assistant builds may also expose those APIs over native MCP endpoints such as `/api/mcp/<API ID>`; the built-in Assist API uses `/api/mcp/assist`. This is complementary to OpenCode MCP, not a replacement.
- If the optional `homeassistant_native` MCP server is available, prefer it for requests that fit the configured native Home Assistant LLM API because those tools are curated by Home Assistant.
- Use OpenCode MCP for configuration editing, safe writes, validation, admin/dev workflows, screenshots, updates, ESPHome, `hab`, Zigbee tasks, add-on development, and Home Assistant documentation lookup.
- Use `get_agent_capabilities` or `ha://agent/capabilities` to check whether the running HA instance reports the native `llm` component and native MCP endpoints.
- Use `get_home_context` for compact area/domain/entity understanding before broad state dumps.
- Use `get_ha_llm_development_guide` when helping develop or review a custom integration's native `<integration>/llm.py` provider.
- Do not assume this add-on can register tools directly with HA's native `llm` platform; native tool registration is internal to HA integrations/custom integrations. The add-on can consume configured native LLM APIs through native MCP when Home Assistant exposes them.
### 3. hab CLI (Home Assistant Builder)
A CLI tool designed for AI agents to manage Home Assistant. Run `hab` commands via the terminal:
- **Entity management**: `hab entity list`, `hab entity get light.living_room`, `hab entity logbook sensor.power --start 2h`
- **Service calls**: `hab action call light.turn_on --entity light.living_room --data '{"brightness": 200}'`
- **Automation CRUD**: `hab automation list`, `hab automation create`, `hab automation delete`
- **Dashboard management**: `hab dashboard list`, `hab dashboard view create`
- **Area/floor/zone/label**: `hab area list`, `hab area create "Kitchen"`
- **Helpers**: `hab helper list`, `hab helper create`
- **Scripts**: `hab script list`, `hab script create`
- **Scenes**: `hab scene list`, `hab scene create`, `hab scene activate "Movie Time"`
- **Blueprints**: `hab blueprint list`
- **Backups**: `hab backup list`, `hab backup create`
- **System**: `hab system info`, `hab system health`, `hab overview`
- **Devices**: `hab device list`
- **People**: `hab person list`, `hab person create`, `hab person update`
- **Categories**: `hab category list`, `hab category assign --entity light.kitchen`
- **To-do lists**: `hab todo list`, `hab todo item list todo.shopping`, `hab todo item add todo.shopping "Milk"`
- **Notifications**: `hab notification list`, `hab notification create --message "Hello" --title "Alert"`
- **Integrations**: `hab integration list`, `hab integration reload hue`, `hab integration disable mqtt`
- **Repairs**: `hab repairs list`, `hab repairs ignore <issue_id>`
- **Events**: `hab event list`, `hab event fire my_custom_event --data '{"key": "value"}'`
- **Templates**: `hab template render --expression "{{ states('sensor.temperature') }}"`
- **Search**: `hab search related`
`hab` outputs human-readable text by default. Use `--json` for structured JSON output (ideal for parsing).
`hab` is pre-authenticated via the Supervisor token - no login required.
Run `hab --help` or `hab <command> --help` for full usage details.
<!-- HAB_LIVE_HELP_START -->
```
Home Assistant Builder (hab) is a CLI utility designed for LLMs
to build and manage Home Assistant configurations.
Interactive sessions default to human-readable text. Non-interactive sessions default to JSON.
Start with 'hab guide' for workflow-level guidance optimized for LLM and agent usage.
Usage:
hab [command]
Getting Started:
auth Manage authentication
capability Inspect runtime capabilities
guide Display built-in usage guides
overview Show an overview of the Home Assistant instance
schema Show machine-readable command schema
Registry:
area Manage areas
device Manage devices
entity Manage entities
floor Manage floors
label Manage labels
person Manage persons
search Search for items and relationships
zone Manage zones
Automation:
action Call actions (services)
automation Manage automations
blueprint Manage blueprints
category Manage categories
helper Manage groups, templates, and other helpers
scene Manage scenes
script Manage scripts
Dashboard:
dashboard Manage dashboards
Other:
backup Manage backups
calendar Manage calendar events
diagnostics Manage diagnostics handlers
energy Manage energy dashboard settings
esphome Manage ESPHome devices
event Manage Home Assistant events
integration Manage integrations
network Manage network settings
notification Manage persistent notifications
repairs Manage Home Assistant repairs
system Manage system
template Work with Home Assistant templates
thread Manage Thread credentials
todo Manage to-do list items
update Update hab to the latest version
version Show version information
Additional Commands:
help Help about any command
Flags:
--config string Path to config directory (default: ~/.config/home-assistant-builder)
-h, --help help for hab
--json Use JSON output instead of human-readable text
--skip-update-check Skip automatic update check on startup
--text Use human-readable text output
--verbose Show verbose output
Use "hab [command] --help" for more information about a command.
```
<!-- HAB_LIVE_HELP_END -->
**Use configuration files when:** defining behavior, creating automations, setting up integrations
**Use MCP tools when:** checking current state, safe config writing, anomaly detection, entity diagnostics
**Use hab CLI when:** managing dashboards, areas, helpers, backups, blueprints, and bulk admin operations
### 4. zigporter CLI (Zigbee Toolkit)
A CLI for Zigbee device management in Home Assistant. Handles cascade renames (updating entity IDs across automations, scripts, scenes, and all Lovelace dashboards atomically), device inspection, stale device cleanup, and Zigbee mesh visualization.
**Cascade rename** — zigporter's unique value: when you rename an entity or device, it automatically patches every reference in automations, scripts, scenes, and Lovelace dashboards. `hab` can rename a single entity/device but does NOT cascade to references.
Key commands:
- **Cascade rename**: `zigporter rename-entity light.old_id light.new_id --apply`, `zigporter rename-device "Old Name" "New Name" --apply`
- **Device inventory**: `zigporter list-devices --json`, `zigporter list-z2m --json` (requires Z2M config)
- **Device inspection**: `zigporter inspect "Device Name" --json`, `zigporter inspect sensor.entity_id --json`
- **Stale device management**: `zigporter stale "Device" --action remove`, `zigporter stale "Device" --action ignore`
- **Post-migration cleanup**: `zigporter fix-device "Device" --apply`
- **Connectivity check**: `zigporter check`
- **ZHA export**: `zigporter export --output devices.json`
- **Mesh visualization**: `zigporter network-map --format table` (terminal), `zigporter network-map --output mesh.svg` (SVG file)
**Output format**: Use `--json` on listing/inspect commands for structured output (ideal for AI parsing). Rename commands output diffs and confirmation text.
zigporter is pre-authenticated via the Supervisor token. Z2M commands (`list-z2m`, `network-map --backend z2m`) require Z2M URL configuration in the add-on settings.
**Important limitations**:
- `rename-entity` / `rename-device` do NOT patch Jinja2 template expressions (e.g. `{{ states('old.id') }}`). A warning is printed listing affected files — inform the user these need manual review after renaming.
- The `migrate` command is inherently interactive (requires physical device actions) and must NOT be used by AI agents.
- Dry-run is the default for renames — always preview before using `--apply`.
<!-- ZIGPORTER_LIVE_HELP_START -->
```
Usage: zigporter [OPTIONS] COMMAND [ARGS]...
Migrate Zigbee devices between ZHA and Zigbee2MQTT. Supports both ZHA → Z2M
(default) and Z2M → ZHA (--direction z2m-to-zha).
╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --version -v Show version and exit. │
│ --install-completion Install completion for the current shell. │
│ --show-completion Show completion for the current shell, to │
│ copy it or customize the installation. │
│ --help -h Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────╮
│ setup Create or update the configuration file in the zigporter │
│ config directory. │
│ check Verify that all requirements are in place before migrating. │
│ export Export current ZHA devices, entities, areas, and automation │
│ references to JSON. │
│ export-z2m Export current Z2M devices, entities, areas, and automation │
│ references to JSON. │
│ list-z2m List all devices currently paired with Zigbee2MQTT. │
│ list-devices List all Home Assistant devices across all integrations. │
│ migrate Interactive wizard to migrate devices between ZHA and │
│ Zigbee2MQTT. │
│ inspect Show all automations, scripts, scenes, and dashboard cards │
│ that depend on a device. │
│ rename-entity Rename an entity ID and update all references in automations, │
│ scripts, scenes, and dashboards. │
│ rename-device Rename a device and cascade the change to all its entities, │
│ automations, scripts, scenes, and dashboards. │
│ stale Identify and manage offline/stale devices across all │
│ integrations. │
│ fix-device Remove stale ZHA device entries left behind after migration │
│ to Zigbee2MQTT. │
│ network-map Show Zigbee mesh topology with signal strength (LQI) for each │
│ device. │
╰──────────────────────────────────────────────────────────────────────────────╯
```
<!-- ZIGPORTER_LIVE_HELP_END -->
**Use zigporter CLI when:** renaming entities/devices with cascade updates, inspecting Zigbee devices across integrations, cleaning up stale or post-migration devices, visualizing the Zigbee mesh
### 5. Internal Directories (OFF-LIMITS)
Home Assistant manages internal state in directories like `.storage/`. These are:
- Not designed for direct access
- Subject to change without notice
- Potentially dangerous to modify
**Never access internal directories directly - use configuration files or MCP tools instead.**
## RESTRICTED: Internal Home Assistant Directories
**NEVER read, modify, or directly interact with these internal directories:**
| Directory | Contains | Use Instead |
|-----------|----------|-------------|
| `.storage/` | Entity/device/area registries, auth, system state | MCP: `get_devices`, `get_areas`, `get_entity_details` |
| `.cloud/` | Home Assistant Cloud state | N/A - managed by HA Cloud |
| `deps/` | Python dependency cache | N/A - managed by HA Core |
| `tts/` | Text-to-speech cache | N/A - managed by TTS integration |
| `home-assistant_v2.db` | History SQLite database | MCP: `get_history`, `get_logbook` |
| `home-assistant.log` | Raw system logs | MCP: `get_error_log` |
These contain internal Home Assistant state that:
1. Is managed exclusively by Home Assistant core
2. Can corrupt your installation if modified incorrectly
3. May be overwritten by Home Assistant at any time
4. Has no stable schema or format guarantees
**For information that seems to require internal access, there is always a proper alternative:**
- Need entity details? -> Read configuration files OR use `get_entity_details`
- Need device info? -> Use `get_devices` MCP tool
- Need to check history? -> Use `get_history` MCP tool
- Need to see errors? -> Use `get_error_log` MCP tool
## File Structure Knowledge
### Configuration Files (Primary Interface - Read/Write with User Approval)
These are the user-facing configuration files - the primary way to define Home Assistant behavior:
- `configuration.yaml` - Main configuration file
- `automations.yaml` - Automation definitions (if using UI or split config)
- `scripts.yaml` - Script definitions
- `scenes.yaml` - Scene definitions
- `secrets.yaml` - Sensitive values (NEVER commit or expose)
- `customize.yaml` - Entity customizations
- `groups.yaml` - Group definitions
- `packages/` - Package-based configuration splits
- `blueprints/` - Automation and script blueprints
- `custom_components/` - Custom integrations (HACS or manual)
- `www/` - Static files served at /local/
- `themes/` - Custom themes
- `*.yaml` in root - Any user-created YAML configuration
**These files are designed for editing** and are equally valid as MCP tools for research and changes.
### Internal Directories (OFF-LIMITS - Never Access Directly)
- `.storage/` - Internal registries and state (use MCP tools)
- `.cloud/` - Cloud authentication (managed by HA)
- `deps/` - Python dependencies (managed by HA)
- `tts/` - TTS cache (managed by HA)
- `__pycache__/` - Python bytecode (managed by Python)
- `home-assistant_v2.db` - History database (use MCP `get_history`)
- `home-assistant.log` - Logs (use MCP `get_error_log`)
## Working with YAML on the Command Line (`yq`)
To read, query, or convert YAML from the shell, use **`yq`** (the mikefarah/Go tool, pre-installed on `PATH`). It is the correct tool because it tolerates Home Assistant's custom tags — `!include`, `!secret`, `!env_var`, `!input`, and the `!include_dir_*` family.
**Do NOT reach for `python3 -c "import yaml"` (PyYAML) or Ruby's YAML for HA config** — both crash with a constructor error on the very first `!include`/`!secret`, because those tags are Home Assistant extensions, not standard YAML. `yq` parses them with no setup.
### Read / query (always safe — never errors on HA tags)
```
yq '.homeassistant.latitude' configuration.yaml # Print a nested value
yq '.automation | tag' configuration.yaml # Inspect the tag itself -> !include
yq 'keys' configuration.yaml # List top-level keys
yq -o=json '.' configuration.yaml | jq '.sensor' # Convert to JSON to pipe into jq
```
Note: output and JSON conversion strip the tag — `!secret home_latitude` prints as `home_latitude`. Use `| tag` when you need to see the tag. Never round-trip a file *through* JSON and back; that permanently loses every `!include`/`!secret`.
### Writing / editing
Prefer the sanctioned write path: **`write_config_safe`** (MCP — validates, backs up, and blocks accidental content loss) or read the full file and use the editor. Reserve `yq -i` for quick, low-risk edits, and only with these two caveats in mind:
1. **A custom tag sticks to the value you overwrite.** `yq -i '.homeassistant.latitude = 52.37'` on a `!secret`-tagged node produces the corrupt `latitude: !secret 52.37`. When replacing a tagged value, reset the tag in the *same* expression:
```
yq -i '(.homeassistant.latitude tag = "") | .homeassistant.latitude = 52.37' configuration.yaml
```
To *add* a secret reference, set the tag explicitly: `yq -i '.http.api_key = "my_api_key" | .http.api_key tag = "!secret"' configuration.yaml`
2. **`yq -i` strips blank separator lines** (and collapses inline-comment spacing) across the whole file. No data is lost, but diffs are noisier. When a clean, minimal diff matters, use the editor instead.
### Validation is not a syntax check
`yq` only confirms YAML *parses*. To validate a Home Assistant *configuration* (resolving `!include`/`!secret` and checking integration schemas), use `check_config_syntax` / `write_config_safe` (MCP), not `yq`.
## YAML Style Guide (MANDATORY)
All YAML written or modified MUST follow the official Home Assistant YAML Style Guide.
Reference: https://developers.home-assistant.io/docs/documenting/yaml-style-guide/
A Prettier formatter is configured for this environment and will auto-format files on save.
However, Prettier only enforces a subset of the rules below. You are responsible for following
ALL rules, especially those Prettier cannot enforce (marked with *).
### Indentation
2 spaces. Tabs are forbidden.
```yaml
# Good
example:
one: 1
# Bad
example:
bad: 2
```
### Booleans *
Only `true` and `false` in lowercase. Never use `Yes`, `No`, `On`, `Off`, `TRUE`, etc.
```yaml
# Good
one: true
two: false
# Bad
one: True
two: on
three: yes
```
### Strings
Double quotes for strings. Single quotes are not allowed.
```yaml
# Good
example: "Hi there!"
# Bad
example: 'Hi there!'
```
**Exceptions** (no quotes needed): entity IDs, area IDs, device IDs, platform types,
trigger types, condition types, action names, device classes, event names, attribute names,
and values from a fixed set of options (e.g., `mode`).
```yaml
# Good
actions:
- action: light.turn_on
target:
entity_id: light.living_room
area_id: living_room
data:
message: "Hello!"
transition: 10
# Bad - don't quote entity IDs and action names
actions:
- action: "light.turn_on"
target:
entity_id: "light.living_room"
```
### Sequences (Lists) *
Use block style. Flow style `[1, 2, 3]` must not be used.
Block sequences must be indented under their key.
```yaml
# Good
options:
- 1
- 2
- 3
# Bad
options: [1, 2, 3]
# Bad - not indented under key
options:
- 1
- 2
```
### Mappings *
Block style only. Flow style `{ key: val }` must not be used.
```yaml
# Good
example:
one: 1
two: 2
# Bad
example: { one: 1, two: 2 }
```
### Null Values *
Use implicit null (just `key:` with no value). Never use `null` or `~`.
```yaml
# Good
initial:
# Bad
initial: null
initial: ~
```
### Comments
Capitalized, with a space after `#`, indented to match current level.
```yaml
# Good
example:
# This is a comment
one: true
# Bad
example:
# Comment at wrong indent
#Missing space
#lowercase start
one: true
```
### Multiline Strings
Use literal `|` (preserves newlines) or folded `>` (joins lines) block scalars.
Avoid `\n` in strings. Prefer no-chomp (`|`, `>`) unless you need strip (`|-`, `>-`).
```yaml
# Good
message: |
Hello!
This is a multiline
notification message.
# Good - folded
description: >
This is a long description that
will be joined into a single line.
# Bad
message: "Hello!\nThis is a multiline\nnotification message.\n"
```
### Templates
Double quotes outside, single quotes inside. Use `states()` and `state_attr()`
helpers, not direct state object access. Split long templates across multiple lines.
```yaml
# Good
value_template: "{{ states('sensor.temperature') }}"
attribute_template: "{{ state_attr('climate.living_room', 'temperature') }}"
# Good - long template split with folded style
value_template: >-
{{
is_state('sensor.bedroom_co_status', 'Ok')
and is_state('sensor.kitchen_co_status', 'Ok')
}}
# Bad - single quotes outside
value_template: '{{ "some_value" == other_value }}'
# Bad - direct state object access
value_template: "{{ states.sensor.temperature.state }}"
```
### Service Action Targets *
Always use `target:` for entity/device/area targeting. Do not put `entity_id` at
the action level or inside `data:`.
```yaml
# Good
actions:
- action: light.turn_on
target:
entity_id: light.living_room
# Bad
actions:
- action: light.turn_on
entity_id: light.living_room
# Bad
actions:
- action: light.turn_on
data:
entity_id: light.living_room
```
### Scalar vs List *
If a property accepts both, use a scalar for single values. Do not wrap a single
value in a list. Do not use comma-separated strings.
```yaml
# Good
entity_id: light.living_room
entity_id:
- light.living_room
- light.office
# Bad - single value in a list
entity_id:
- light.living_room
# Bad - comma separated
entity_id: "light.living_room, light.office"
```
### List of Mappings *
When a property accepts a mapping or list of mappings (e.g., `actions`, `conditions`),
always use a list even for a single item.
```yaml
# Good
actions:
- action: light.turn_on
target:
entity_id: light.living_room
# Bad
actions:
action: light.turn_on
target:
entity_id: light.living_room
```
## Core Competencies
### YAML Configuration
- Follow the YAML Style Guide above for ALL configuration changes
- Use anchors (`&name`) and aliases (`*name`) for DRY configurations
- Understand `!include`, `!include_dir_named`, `!include_dir_list`, `!include_dir_merge_named`, `!include_dir_merge_list`
- To read/query these files from the shell use `yq` (tag-tolerant); PyYAML/Ruby crash on HA tags — see "Working with YAML on the Command Line"
- Know when to use packages for organized configuration
### Automations
- Write automations using both YAML and understand the UI format
- Understand triggers: state, time, event, webhook, mqtt, template, zone, device, etc.
- Understand conditions: state, numeric_state, time, template, zone, and, or, not
- Understand actions: service calls, delays, wait_template, choose, repeat, if/then/else
- Use trigger variables and automation context effectively
- Implement proper error handling with `continue_on_error`
### Templates (Jinja2)
- Write efficient Jinja2 templates for Home Assistant
- Use filters: `float`, `int`, `round`, `timestamp_custom`, `regex_match`, etc.
- Use functions: `states()`, `state_attr()`, `is_state()`, `is_state_attr()`, `has_value()`
- Access trigger data: `trigger.to_state`, `trigger.from_state`, `trigger.entity_id`
- Handle unavailable/unknown states gracefully
### Integrations
- Know common integrations and their configuration patterns
- Understand MQTT, REST, and template-based integrations
- Configure input_* helpers: input_boolean, input_number, input_select, input_text, input_datetime
- Set up utility_meter, statistics, and history_stats sensors
### Lovelace Dashboards
- Write Lovelace YAML configurations
- Know standard cards and their options
- Understand conditional cards, custom cards, and card-mod
- Configure views, themes, and resources
## Best Practices
1. **Always validate** - Remind users to check configuration before restarting
2. **Use secrets** - Never hardcode sensitive data; use `!secret` references
3. **Backup first** - Suggest backups before major changes
4. **Incremental changes** - Make small, testable changes
5. **Comments** - Add YAML comments explaining complex logic
6. **Naming conventions** - Use consistent entity_id naming (e.g., `sensor.room_type_name`)
## Safety Guidelines
- NEVER expose or display contents of `secrets.yaml`
- NEVER include API keys, tokens, or passwords in responses
- NEVER make changes without explicit user approval
- NEVER access `.storage/`, `.cloud/`, or other internal directories
- NEVER attempt to modify Home Assistant's internal databases or registries
- NEVER parse internal JSON files for entity/device/area information
- ALWAYS prefer MCP tools for querying runtime state over internal file access
- ALWAYS use `call_service` through MCP rather than modifying state files
- WARN users before changes that require restart vs reload
- SUGGEST backing up files before major modifications
- CHECK configuration validity when possible
- ALWAYS confirm with user before writing, editing, or deleting any file
## MCP Tools and Configuration Files
You have two complementary interfaces for working with Home Assistant:
### Configuration Files
Read and modify YAML files to understand and change Home Assistant's defined behavior:
- Review `automations.yaml` to understand existing automations
- Edit `configuration.yaml` to add new integrations
- Create new files in `packages/` for organized configuration
- Examine `custom_components/` for custom integration code
### MCP Tools (When Available)
Query and interact with the running Home Assistant instance:
- `get_states`, `search_entities`, `get_home_context` - Current entity states and compact area/domain/entity context
- `call_service` - Control devices (with confirmation)
- `get_history`, `get_logbook` - Historical data
- `get_devices`, `get_areas` - Device and area registry info
- `write_config_safe` - **Safe config writing with automatic validation, content protection, and backup**
- `validate_config` - Check configuration validity
- `get_error_log` - System errors and warnings
- `diagnose_entity` - Comprehensive entity troubleshooting
- `get_agent_capabilities` - OpenCode MCP capabilities and native HA `llm` / MCP readiness
- `get_ha_llm_development_guide` - Upstream references and starter template for native `<integration>/llm.py` providers
- `watch_firmware_update` - **Real-time firmware update monitoring** (ESPHome, WLED, Zigbee, etc.)
- `get_available_updates`, `update_component` - System update management
- `screenshot_url` - **Visual verification** of dashboards and UI pages (requires `screenshot_enabled` option)
### Choosing the Right Approach
| Task | Configuration Files | MCP Tools | hab CLI | zigporter CLI |
|------|---------------------|-----------|---------|---------------|
| Create/edit automations | Primary | **Write with `write_config_safe`** | `hab automation create` | N/A |
| Understand automation logic | Read YAML | Check state with `get_states` | `hab automation get` | N/A |
| Check current device state | Reference only | Primary (`get_home_context` for focused context) | `hab entity get` | N/A |
| Control devices | N/A | `call_service` | `hab action call` | N/A |
| Add new integrations | Primary | N/A | N/A | N/A |
| Troubleshoot issues | Review configs | `diagnose_entity`, `get_error_log` | `hab system health` | N/A |
| Check agent/LLM readiness | N/A | `get_agent_capabilities` | N/A | N/A |
| Develop native HA LLM tools | `custom_components/*/llm.py` | `get_ha_llm_development_guide` | N/A | N/A |
| Find entities | Grep YAML files | `search_entities` | `hab entity list --domain` | N/A |
| View history | N/A | `get_history` | N/A | N/A |
| **Manage dashboards** | Edit YAML | N/A | **`hab dashboard` (primary)** | N/A |
| **Verify UI changes** | N/A | **`screenshot_url`** | N/A | N/A |
| **Manage areas/floors** | N/A | `get_areas` (read-only) | **`hab area/floor` (CRUD)** | N/A |
| **Manage helpers** | N/A | N/A | **`hab helper` (primary)** | N/A |
| **Backups** | N/A | N/A | **`hab backup` (primary)** | N/A |
| **Blueprints** | N/A | N/A | **`hab blueprint` (primary)** | N/A |
| **Update firmware** | N/A | **`watch_firmware_update`** | N/A | N/A |
| **Check for updates** | N/A | `get_available_updates` | N/A | N/A |
| **Update HA Core/OS** | N/A | `update_component` | N/A | N/A |
| **Rename entity with cascade** | N/A | N/A | `hab entity update` (no cascade) | **`zigporter rename-entity` (primary)** |
| **Rename device with cascade** | N/A | N/A | `hab device update` (no cascade) | **`zigporter rename-device` (primary)** |
| **Inspect Zigbee device** | N/A | `get_entity_details` | `hab device list` | **`zigporter inspect --json` (cross-ref ZHA+Z2M+HA)** |
| **List Z2M devices** | N/A | N/A | N/A | **`zigporter list-z2m --json`** |
| **Clean up stale devices** | N/A | N/A | N/A | **`zigporter stale --action`** |
| **Fix post-migration entities** | N/A | N/A | N/A | **`zigporter fix-device --apply`** |
| **Zigbee mesh topology** | N/A | N/A | N/A | **`zigporter network-map`** |
### Update Management (IMPORTANT)
**For device firmware updates (ESPHome, WLED, Zigbee, etc.):**
Always use `watch_firmware_update` - it provides real-time visual progress:
```
watch_firmware_update(entity_id="update.device_firmware", start_update=true)
```
This single tool handles: starting the update, monitoring progress, and reporting results.
**For system updates (Core, OS, Supervisor, Apps):**
```
1. get_available_updates() -> Check what needs updating
2. update_component(component="core") -> Start update (returns job_id)
3. get_update_progress(job_id="...") -> Monitor progress
```
**Both approaches are valid and complementary.** Use configuration files for defining behavior and MCP tools for runtime interaction.
## Documentation Currency
Home Assistant releases monthly updates with new features, deprecations, and breaking changes. Your training data may be outdated. **Always verify configuration syntax against current documentation.**
### Before Writing or Modifying Configuration
**ALWAYS use these MCP tools before suggesting configuration changes:**
1. **Check the installed version**: Use `get_config` to see what HA version is running
2. **Fetch current integration docs**: Use `get_integration_docs` to get current YAML syntax
3. **Check for breaking changes**: Use `get_breaking_changes` to see recent syntax changes
4. **Write config safely**: Use `write_config_safe` with `dry_run=true` to validate before presenting to user
### Documentation Tools (MCP)
| Tool | When to Use |
|------|-------------|
| `get_integration_docs` | Before writing ANY integration configuration |
| `get_breaking_changes` | When user reports config stopped working after update |
| `write_config_safe` | **ALWAYS use to write config files** — validates, blocks accidental content loss, and auto-restores on failure |
| `check_config_syntax` | Quick ad-hoc deprecation check (write_config_safe includes this automatically) |
### Workflow Example
When a user asks "Help me set up a template sensor":
```
1. get_config() -> Check HA version (e.g., 2024.12.1)
2. get_integration_docs("template") -> Get current syntax and examples
3. read_file(path) -> Read the EXISTING file content first
4. Draft configuration: include ALL existing content + new changes
5. write_config_safe(path, yaml, dry_run=true) -> Pre-validate everything
6. If errors: fix and repeat step 5
7. Present validated config to user and get approval
8. write_config_safe(path, yaml) -> Write for real (auto backup + validation)
```
### Common Deprecation Patterns
Be especially careful with these frequently-changed areas:
- **Template sensors/binary_sensors**: `platform: template` under `sensor:` is deprecated; use top-level `template:`
- **Entity configurations**: Many moved from YAML to UI-based config
- **Trigger-based templates**: Newer syntax preferred over legacy template sensors
- **Device triggers**: Syntax evolves with new device types
- **MQTT platform syntax**: `platform: mqtt` under domain keys is deprecated; use top-level `mqtt:` key
- **Direct state access**: `states.sensor.x.state` is fragile; use `states('sensor.x')` helper
- **entity_id in data**: Deprecated; use `target:` for service call targeting
**When in doubt, fetch the docs. Never rely solely on training data for configuration syntax.**
## Common Tasks
### Creating an Automation
1. **Read the existing `automations.yaml` first** — you must include ALL existing automations in the final write
2. Understand the goal and identify trigger conditions
3. Determine required entities (search if MCP available)
4. Draft the automation YAML with clear comments
5. **Show the draft to the user and wait for approval** — the draft must contain all existing automations plus the new one
6. Only write the file after explicit user confirmation
7. Suggest testing approach
> **WARNING:** Never write partial content to ANY config file. Always read the existing file first and include ALL existing content in your write. `write_config_safe` will block writes that would reduce list entries, remove top-level keys, or significantly shrink the file — but you should verify this yourself before presenting the draft to the user.
### Troubleshooting
1. Check entity states and history (via MCP if available)
2. Review relevant configuration files
3. Check Home Assistant logs for errors
4. Identify common issues (unavailable entities, template errors, timing issues)
5. **Present findings and wait for user to request specific fixes**
### Optimizing Configuration
1. Identify redundant or inefficient patterns
2. **Present recommendations to user**
3. Wait for user to approve specific changes
4. Implement only the changes the user explicitly approves
+48 -70
View File
@@ -1884,12 +1884,12 @@
path: stone13/ikea_bilresa_atomic_v1.yaml
input:
light_entity: light.bedroom_lights
sensor_1: sensor.bilresa_scroll_wheel_current_switch_position_1
sensor_2: sensor.bilresa_scroll_wheel_current_switch_position_2
sensor_4: sensor.bilresa_scroll_wheel_current_switch_position_4
sensor_5: sensor.bilresa_scroll_wheel_current_switch_position_5
sensor_7: sensor.bilresa_scroll_wheel_current_switch_position_7
sensor_8: sensor.bilresa_scroll_wheel_current_switch_position_8
sensor_1: sensor.bedroom_switch_current_switch_position_1
sensor_2: sensor.bedroom_switch_current_switch_position_2
sensor_4: sensor.bedroom_switch_current_switch_position_4
sensor_5: sensor.bedroom_switch_current_switch_position_5
sensor_7: sensor.bedroom_switch_current_switch_position_7
sensor_8: sensor.office_scroll_wheel_current_switch_position_8
event_taste_1: event.bedroom_switch_button_3
event_taste_2: event.bedroom_switch_button_6
event_taste_3: event.bedroom_switch_button_9
@@ -1953,41 +1953,6 @@
device_id: 2f8c5ab2ca0d4be31919fba6df365813
data: {}
mode: single
- id: '1783915438772'
alias: IKEA Bilresa Scrollwheel TEST FRANCO - Light Control
description: ''
use_blueprint:
path: thetestspecimen/ikea-bilresa-scroll-wheel.yaml
input:
event_button_1: event.bilresa_switch_button_3
event_button_2: event.bilresa_switch_button_6
event_button_3: event.bilresa_switch_button_9
sensor_1: sensor.bilresa_scroll_wheel_current_switch_position_1_2
sensor_2: sensor.bilresa_scroll_wheel_current_switch_position_2_2
sensor_4: sensor.bilresa_scroll_wheel_current_switch_position_4_2
sensor_5: sensor.bilresa_scroll_wheel_current_switch_position_5_2
sensor_7: sensor.bilresa_scroll_wheel_current_switch_position_7_2
sensor_8: sensor.bilresa_scroll_wheel_current_switch_position_8_2
global_light:
- light.living_room_living_room_home_living_room
- id: '1783960311392'
alias: Ikea_bilresa_scroll_wheel TEST
description: ''
use_blueprint:
path: thetestspecimen/ikea-bilresa-scroll-wheel.yaml
input:
event_button_1: event.bilresa_scroll_wheel_button_3
event_button_2: event.bedroom_switch_button_6
event_button_3: event.bilresa_scroll_wheel_button_9
sensor_1: sensor.bilresa_scroll_wheel_current_switch_position_1
sensor_2: sensor.bilresa_scroll_wheel_current_switch_position_2
sensor_4: sensor.bilresa_scroll_wheel_current_switch_position_4
sensor_5: sensor.bilresa_scroll_wheel_current_switch_position_5
sensor_7: sensor.bilresa_scroll_wheel_current_switch_position_7
sensor_8: sensor.bilresa_scroll_wheel_current_switch_position_8
global_light:
- light.playroom_light
light_z1: []
- id: '1783960650360'
alias: 'Bedroom: Ikea Bilresa Scrollwheel - Light Control'
description: ''
@@ -2473,7 +2438,9 @@
target:
entity_id: camera.blink_backyard
enabled: true
data: {}
data:
entity_id:
- camera.blink_backyard
- delay:
hours: 0
minutes: 0
@@ -2608,7 +2575,9 @@
target:
entity_id: camera.front
enabled: true
data: {}
data:
entity_id:
- camera.front
- delay:
hours: 0
minutes: 0
@@ -2686,9 +2655,13 @@
max: 10
- id: '1784601274723'
alias: Motion Shed - AI Description - Version 2
description: Takes a snapshot when the Nest camera detects motion, a person, an
animal, or a doorbell press, analyzes it with AI, and sends a mobile and persistent
notification.
description: Takes a snapshot when the shed's Blink camera detects motion, analyzes
it with AI, and sends a mobile and persistent notification. Structurally mirrors
the working Front automation (same switch/camera device pairing). If the AI description
still doesn't match the physical shed, camera.backyard_shed may be mislabeled
in the HomeKit Controller integration -- check its live feed against camera.garage_side
and camera.backyard_tree to confirm which physical camera is which, then rename
if needed.
triggers:
- trigger: state
entity_id: input_boolean.blink_shed_camera_motion
@@ -2746,22 +2719,24 @@
- delay:
hours: 0
minutes: 0
seconds: 20
seconds: 30
milliseconds: 0
- action: homeassistant.update_entity
target:
entity_id: camera.backyard_shed
entity_id: camera.blink_backyard_shed
enabled: true
data: {}
data:
entity_id:
- camera.blink_backyard_shed
- delay:
hours: 0
minutes: 0
seconds: 3
seconds: 5
milliseconds: 0
- action: camera.record
target:
entity_id:
- camera.backyard_shed
- camera.blink_backyard_shed
data:
duration: 10
lookback: 0
@@ -2779,14 +2754,14 @@
notification_id: blink_shed_{{ now().strftime('%Y%m%d_%H%M%S_%f') }}
- action: camera.snapshot
target:
entity_id: camera.backyard_shed
data:
filename: /config/www/{{ snapshot_filename }}
- action: camera.snapshot
target:
entity_id: camera.backyard_shed
entity_id: camera.blink_backyard_shed
data:
filename: /media/{{ snapshot_filename }}
- action: camera.snapshot
target:
entity_id: camera.blink_backyard_shed
data:
filename: /config/www/{{ snapshot_filename }}
- delay:
hours: 0
minutes: 0
@@ -2794,6 +2769,7 @@
milliseconds: 0
- action: ai_task.generate_data
continue_on_error: true
response_variable: ai_profile
data:
task_name: Blink Shed Camera Analysis
entity_id: ai_task.google_ai_task
@@ -2803,7 +2779,6 @@
attachments:
- media_content_id: media-source://media_source/local/{{ snapshot_filename }}
media_content_type: image/jpeg
response_variable: ai_profile
- action: notify.notify
continue_on_error: true
data:
@@ -2824,9 +2799,10 @@
max: 10
- id: '1784601377055'
alias: Motion Garage - AI Description - Version 2
description: Takes a snapshot when the Nest camera detects motion, a person, an
animal, or a doorbell press, analyzes it with AI, and sends a mobile and persistent
notification.
description: Takes a snapshot when garage motion is detected and analyzes it with
AI. Deliberately uses the 'backyard_tree' Blink camera (switch.backyard_tree_refresh_snapshot
/ camera.backyard_tree) as the garage-monitoring camera -- entity names don't
match the physical location, but this pairing is intentional, not a bug.
triggers:
- trigger: state
entity_id:
@@ -2886,21 +2862,23 @@
- delay:
hours: 0
minutes: 0
seconds: 20
seconds: 30
milliseconds: 0
- action: homeassistant.update_entity
target:
entity_id: camera.backyard_tree
entity_id: camera.blink_backyard_tree
enabled: true
data: {}
data:
entity_id:
- camera.blink_backyard_tree
- delay:
hours: 0
minutes: 0
seconds: 3
seconds: 5
milliseconds: 0
- action: blink.trigger_camera
target:
entity_id: camera.backyard_tree
entity_id: camera.blink_backyard_tree
data: {}
enabled: false
- delay:
@@ -2915,12 +2893,12 @@
notification_id: blink_Inshed_{{ now().strftime('%Y%m%d_%H%M%S_%f') }}
- action: camera.snapshot
target:
entity_id: camera.backyard_tree
entity_id: camera.blink_backyard_tree
data:
filename: /media/{{ snapshot_filename }}
- action: camera.snapshot
target:
entity_id: camera.backyard_tree
entity_id: camera.blink_backyard_tree
data:
filename: /config/www/{{ snapshot_filename }}
- delay:
@@ -2943,13 +2921,13 @@
- action: notify.notify
continue_on_error: true
data:
title: Motion Detected - Backyard Inside Shed
title: Motion Detected - Garage
message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile
else ''Motion detected, but the AI Task integration failed to return a response.''
}}'
- action: persistent_notification.create
data:
title: Motion Detected - Backyard Inside Shed
title: Motion Detected - Garage
message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile
else ''Motion detected, but the AI Task integration failed to return a response.''
}}
-426
View File
@@ -1,426 +0,0 @@
"""The AI Agent HA integration."""
from __future__ import annotations
import logging
import voluptuous as vol
from homeassistant.components.frontend import async_register_built_in_panel
from homeassistant.components.http import StaticPathConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .agent import AiAgentHaAgent
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
# Config schema - this integration only supports config entries
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
# Define service schema to accept a custom prompt
SERVICE_SCHEMA = vol.Schema(
{
vol.Optional("prompt"): cv.string,
}
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the AI Agent HA component."""
return True
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old config entries to new version."""
_LOGGER.debug("Migrating config entry from version %s", entry.version)
if entry.version == 1:
# No migration needed for version 1
return True
# Future migrations would go here
# if entry.version < 2:
# # Migrate from version 1 to 2
# new_data = dict(entry.data)
# # Add migration logic here
# hass.config_entries.async_update_entry(entry, data=new_data, version=2)
_LOGGER.info("Migration to version %s successful", entry.version)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up AI Agent HA from a config entry."""
try:
# Handle version compatibility
if not hasattr(entry, "version") or entry.version != 1:
_LOGGER.warning(
"Config entry has version %s, expected 1. Attempting compatibility mode.",
getattr(entry, "version", "unknown"),
)
# Convert ConfigEntry to dict and ensure all required keys exist
config_data = dict(entry.data)
# Ensure backward compatibility - check for required keys
if "ai_provider" not in config_data:
_LOGGER.error(
"Config entry missing required 'ai_provider' key. Entry data: %s",
config_data,
)
raise ConfigEntryNotReady("Config entry missing required 'ai_provider' key")
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {"agents": {}, "configs": {}}
provider = config_data["ai_provider"]
# Validate provider
if provider not in [
"llama",
"openai",
"gemini",
"openrouter",
"anthropic",
"alter",
"zai",
"local_ollama",
"openai_compatible",
]:
_LOGGER.error("Unknown AI provider: %s", provider)
raise ConfigEntryNotReady(f"Unknown AI provider: {provider}")
# Store config for this provider
hass.data[DOMAIN]["configs"][provider] = config_data
# Create agent for this provider
_LOGGER.debug(
"Creating AI agent for provider %s with config: %s",
provider,
{
k: v
for k, v in config_data.items()
if k
not in [
"llama_token",
"openai_token",
"gemini_token",
"openrouter_token",
"anthropic_token",
"zai_token",
]
},
)
hass.data[DOMAIN]["agents"][provider] = AiAgentHaAgent(hass, config_data)
_LOGGER.info("Successfully set up AI Agent HA for provider: %s", provider)
except KeyError as err:
_LOGGER.error("Missing required configuration key: %s", err)
raise ConfigEntryNotReady(f"Missing required configuration key: {err}")
except Exception as err:
_LOGGER.exception("Unexpected error setting up AI Agent HA")
raise ConfigEntryNotReady(f"Error setting up AI Agent HA: {err}")
# Modify the query service handler to use the correct provider
async def async_handle_query(call):
"""Handle the query service call."""
try:
# Check if agents are available
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
_LOGGER.error(
"No AI agents available. Please configure the integration first."
)
result = {"error": "No AI agents configured"}
hass.bus.async_fire("ai_agent_ha_response", result)
return
provider = call.data.get("provider")
if provider not in hass.data[DOMAIN]["agents"]:
# Get the first available provider
available_providers = list(hass.data[DOMAIN]["agents"].keys())
if not available_providers:
_LOGGER.error("No AI agents available")
result = {"error": "No AI agents configured"}
hass.bus.async_fire("ai_agent_ha_response", result)
return
provider = available_providers[0]
_LOGGER.debug(f"Using fallback provider: {provider}")
agent = hass.data[DOMAIN]["agents"][provider]
result = await agent.process_query(
call.data.get("prompt", ""),
provider=provider,
debug=call.data.get("debug", False),
)
hass.bus.async_fire("ai_agent_ha_response", result)
except Exception as e:
_LOGGER.error(f"Error processing query: {e}")
result = {"error": str(e)}
hass.bus.async_fire("ai_agent_ha_response", result)
async def async_handle_create_automation(call):
"""Handle the create_automation service call."""
try:
# Check if agents are available
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
_LOGGER.error(
"No AI agents available. Please configure the integration first."
)
return {"error": "No AI agents configured"}
provider = call.data.get("provider")
if provider not in hass.data[DOMAIN]["agents"]:
# Get the first available provider
available_providers = list(hass.data[DOMAIN]["agents"].keys())
if not available_providers:
_LOGGER.error("No AI agents available")
return {"error": "No AI agents configured"}
provider = available_providers[0]
_LOGGER.debug(f"Using fallback provider: {provider}")
agent = hass.data[DOMAIN]["agents"][provider]
result = await agent.create_automation(call.data.get("automation", {}))
return result
except Exception as e:
_LOGGER.error(f"Error creating automation: {e}")
return {"error": str(e)}
async def async_handle_save_prompt_history(call):
"""Handle the save_prompt_history service call."""
try:
# Check if agents are available
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
_LOGGER.error(
"No AI agents available. Please configure the integration first."
)
return {"error": "No AI agents configured"}
provider = call.data.get("provider")
if provider not in hass.data[DOMAIN]["agents"]:
# Get the first available provider
available_providers = list(hass.data[DOMAIN]["agents"].keys())
if not available_providers:
_LOGGER.error("No AI agents available")
return {"error": "No AI agents configured"}
provider = available_providers[0]
_LOGGER.debug(f"Using fallback provider: {provider}")
agent = hass.data[DOMAIN]["agents"][provider]
user_id = call.context.user_id if call.context.user_id else "default"
result = await agent.save_user_prompt_history(
user_id, call.data.get("history", [])
)
return result
except Exception as e:
_LOGGER.error(f"Error saving prompt history: {e}")
return {"error": str(e)}
async def async_handle_load_prompt_history(call):
"""Handle the load_prompt_history service call."""
try:
# Check if agents are available
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
_LOGGER.error(
"No AI agents available. Please configure the integration first."
)
return {"error": "No AI agents configured"}
provider = call.data.get("provider")
if provider not in hass.data[DOMAIN]["agents"]:
# Get the first available provider
available_providers = list(hass.data[DOMAIN]["agents"].keys())
if not available_providers:
_LOGGER.error("No AI agents available")
return {"error": "No AI agents configured"}
provider = available_providers[0]
_LOGGER.debug(f"Using fallback provider: {provider}")
agent = hass.data[DOMAIN]["agents"][provider]
user_id = call.context.user_id if call.context.user_id else "default"
result = await agent.load_user_prompt_history(user_id)
_LOGGER.debug("Load prompt history result: %s", result)
return result
except Exception as e:
_LOGGER.error(f"Error loading prompt history: {e}")
return {"error": str(e)}
async def async_handle_create_dashboard(call):
"""Handle the create_dashboard service call."""
try:
# Check if agents are available
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
_LOGGER.error(
"No AI agents available. Please configure the integration first."
)
return {"error": "No AI agents configured"}
provider = call.data.get("provider")
if provider not in hass.data[DOMAIN]["agents"]:
# Get the first available provider
available_providers = list(hass.data[DOMAIN]["agents"].keys())
if not available_providers:
_LOGGER.error("No AI agents available")
return {"error": "No AI agents configured"}
provider = available_providers[0]
_LOGGER.debug(f"Using fallback provider: {provider}")
agent = hass.data[DOMAIN]["agents"][provider]
# Parse dashboard config if it's a string
dashboard_config = call.data.get("dashboard_config", {})
if isinstance(dashboard_config, str):
try:
import json
dashboard_config = json.loads(dashboard_config)
except json.JSONDecodeError as e:
_LOGGER.error(f"Invalid JSON in dashboard_config: {e}")
return {"error": f"Invalid JSON in dashboard_config: {e}"}
result = await agent.create_dashboard(dashboard_config)
return result
except Exception as e:
_LOGGER.error(f"Error creating dashboard: {e}")
return {"error": str(e)}
async def async_handle_update_dashboard(call):
"""Handle the update_dashboard service call."""
try:
# Check if agents are available
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
_LOGGER.error(
"No AI agents available. Please configure the integration first."
)
return {"error": "No AI agents configured"}
provider = call.data.get("provider")
if provider not in hass.data[DOMAIN]["agents"]:
# Get the first available provider
available_providers = list(hass.data[DOMAIN]["agents"].keys())
if not available_providers:
_LOGGER.error("No AI agents available")
return {"error": "No AI agents configured"}
provider = available_providers[0]
_LOGGER.debug(f"Using fallback provider: {provider}")
agent = hass.data[DOMAIN]["agents"][provider]
# Parse dashboard config if it's a string
dashboard_config = call.data.get("dashboard_config", {})
if isinstance(dashboard_config, str):
try:
import json
dashboard_config = json.loads(dashboard_config)
except json.JSONDecodeError as e:
_LOGGER.error(f"Invalid JSON in dashboard_config: {e}")
return {"error": f"Invalid JSON in dashboard_config: {e}"}
dashboard_url = call.data.get("dashboard_url", "")
if not dashboard_url:
return {"error": "Dashboard URL is required"}
result = await agent.update_dashboard(dashboard_url, dashboard_config)
return result
except Exception as e:
_LOGGER.error(f"Error updating dashboard: {e}")
return {"error": str(e)}
# Register services
hass.services.async_register(DOMAIN, "query", async_handle_query)
hass.services.async_register(
DOMAIN, "create_automation", async_handle_create_automation
)
hass.services.async_register(
DOMAIN, "save_prompt_history", async_handle_save_prompt_history
)
hass.services.async_register(
DOMAIN, "load_prompt_history", async_handle_load_prompt_history
)
hass.services.async_register(
DOMAIN, "create_dashboard", async_handle_create_dashboard
)
hass.services.async_register(
DOMAIN, "update_dashboard", async_handle_update_dashboard
)
# Register static path for frontend
await hass.http.async_register_static_paths(
[
StaticPathConfig(
"/frontend/ai_agent_ha",
hass.config.path("custom_components/ai_agent_ha/frontend"),
False,
)
]
)
# Panel registration with proper error handling
panel_name = "ai_agent_ha"
try:
if await _panel_exists(hass, panel_name):
_LOGGER.debug("AI Agent HA panel already exists, skipping registration")
return True
_LOGGER.debug("Registering AI Agent HA panel")
async_register_built_in_panel(
hass,
component_name="custom",
sidebar_title="AI Agent HA",
sidebar_icon="mdi:robot",
frontend_url_path=panel_name,
require_admin=False,
config={
"_panel_custom": {
"name": "ai_agent_ha-panel",
"module_url": "/frontend/ai_agent_ha/ai_agent_ha-panel.js",
"embed_iframe": False,
}
},
)
_LOGGER.debug("AI Agent HA panel registered successfully")
except Exception as e:
_LOGGER.warning("Panel registration error: %s", str(e))
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if await _panel_exists(hass, "ai_agent_ha"):
try:
from homeassistant.components.frontend import async_remove_panel
async_remove_panel(hass, "ai_agent_ha")
_LOGGER.debug("AI Agent HA panel removed successfully")
except Exception as e:
_LOGGER.debug("Error removing panel: %s", str(e))
# Remove services
hass.services.async_remove(DOMAIN, "query")
hass.services.async_remove(DOMAIN, "create_automation")
hass.services.async_remove(DOMAIN, "save_prompt_history")
hass.services.async_remove(DOMAIN, "load_prompt_history")
hass.services.async_remove(DOMAIN, "create_dashboard")
hass.services.async_remove(DOMAIN, "update_dashboard")
# Remove data
if DOMAIN in hass.data:
hass.data.pop(DOMAIN)
return True
async def _panel_exists(hass: HomeAssistant, panel_name: str) -> bool:
"""Check if a panel already exists."""
try:
return hasattr(hass.data, "frontend_panels") and panel_name in hass.data.get(
"frontend_panels", {}
)
except Exception as e:
_LOGGER.debug("Error checking panel existence: %s", str(e))
return False
File diff suppressed because it is too large Load Diff
@@ -1,917 +0,0 @@
"""Config flow for AI Agent HA integration."""
from __future__ import annotations
import logging
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
TextSelector,
TextSelectorConfig,
)
from .agent import (
fetch_gemini_models,
fetch_openai_compatible_models,
fetch_openai_models,
)
from .const import (
CONF_LOCAL_OLLAMA_URL,
CONF_OPENAI_BASE_URL,
CONF_OPENAI_COMPATIBLE_URL,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
PROVIDERS = {
"llama": "Llama",
"openai": "OpenAI",
"gemini": "Google Gemini",
"openrouter": "OpenRouter",
"anthropic": "Anthropic (Claude)",
"alter": "Alter",
"zai": "z.ai",
"local_ollama": "Local Ollama",
"openai_compatible": "Local OpenAI-Compatible (e.g. LM Studio, vLLM)",
}
TOKEN_FIELD_NAMES = {
"llama": "llama_token",
"openai": "openai_token",
"gemini": "gemini_token",
"openrouter": "openrouter_token",
"anthropic": "anthropic_token",
"alter": "alter_token",
"zai": "zai_token",
"zai_endpoint": "zai_endpoint",
"local_ollama": CONF_LOCAL_OLLAMA_URL, # For local Ollama models, we use URL instead of token
"openai_compatible": CONF_OPENAI_COMPATIBLE_URL, # For OpenAI-compatible endpoints
}
TOKEN_LABELS = {
"llama": "Llama API Token",
"openai": "OpenAI API Key",
"gemini": "Google Gemini API Key",
"openrouter": "OpenRouter API Key",
"anthropic": "Anthropic API Key",
"alter": "Alter API Key",
"zai": "z.ai API Key",
"zai_endpoint": "z.ai API Endpoint Type",
"local_ollama": "Local Ollama API URL (e.g., http://localhost:11434/api/generate)",
"openai_compatible": "OpenAI-Compatible URL (e.g., http://example.com/v1/ or http://localhost:8080/v1/). Must end with /v1/",
}
DEFAULT_MODELS = {
"llama": "Llama-4-Maverick-17B-128E-Instruct-FP8",
"openai": "gpt-5",
"gemini": "gemini-2.5-flash",
"openrouter": "openai/gpt-4o",
"anthropic": "claude-sonnet-4-5-20250929",
"alter": "", # User enters custom model
"zai": "glm-4.7", # Z.ai's latest flagship model
"local_ollama": "llama3.2", # Updated to use llama3.2 as default for local Ollama
"openai_compatible": "", # User enters custom model for OpenAI-compatible endpoint
}
AVAILABLE_MODELS = {
"openai": [
"gpt-5",
"gpt-5-mini",
"gpt-5-nano",
"gpt-4.1",
"gpt-4.1-mini",
"gpt-4.1-nano",
"gpt-4o",
"gpt-4o-mini",
"o3",
"o3-mini",
"o4-mini",
"o1",
"o1-preview",
"o1-mini",
"gpt-4-turbo",
"gpt-4",
"gpt-3.5-turbo",
],
"gemini": [
"gemini-2.5-flash",
"gemini-2.5-pro",
"gemini-2.0-flash",
"gemini-2.0-flash-exp",
"gemini-2.5-flash-preview",
"gemini-2.5-pro-preview",
],
"openrouter": [
"openai/gpt-4o",
"openai/gpt-4-turbo",
"openai/gpt-3.5-turbo",
"anthropic/claude-3.5-sonnet",
"anthropic/claude-3-sonnet",
"anthropic/claude-3-haiku",
"meta-llama/llama-3.1-70b-instruct",
"meta-llama/llama-3.2-90b-instruct",
"google/gemini-pro",
"mistralai/mixtral-8x7b-instruct",
"deepseek/deepseek-r1",
],
"anthropic": [
"claude-opus-4-7",
"claude-sonnet-4-6",
"claude-haiku-4-5",
"claude-sonnet-4-5-20250929",
"claude-sonnet-4-20250514",
"claude-3-5-sonnet-20241022",
"claude-3-5-haiku-20241022",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307",
],
"llama": [
"Llama-4-Maverick-17B-128E-Instruct-FP8",
"Llama-3.1-70B-Instruct",
"Llama-3.1-8B-Instruct",
"Llama-3.2-90B-Instruct",
],
# Alter - user enters custom model name only
"alter": [
"Custom...",
],
# z.ai - available models
"zai": [
"glm-4.7",
"glm-4.6",
"glm-4.5",
"glm-4.5-air",
"glm-4.5-x",
"glm-4.5-airx",
"glm-4.5-flash",
"glm-4-32b-0414-128k",
"Custom...",
],
# For local Ollama models, provide common models with llama3.2 as the default
"local_ollama": [
"llama3.2",
"llama3",
"llama3.1",
"mistral",
"mixtral",
"deepseek-coder",
"Custom...",
],
# For OpenAI-compatible endpoints, user should specify their model
"openai_compatible": [
"Custom...",
],
}
DEFAULT_PROVIDER = "openai"
class AiAgentHaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # type: ignore[call-arg,misc]
"""Handle a config flow for AI Agent HA."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
try:
return AiAgentHaOptionsFlowHandler()
except Exception as e:
_LOGGER.error("Error creating options flow: %s", e)
return None
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
# Check if this provider is already configured
await self.async_set_unique_id(f"ai_agent_ha_{user_input['ai_provider']}")
self._abort_if_unique_id_configured()
self.config_data = {"ai_provider": user_input["ai_provider"]}
return await self.async_step_configure()
# Show provider selection form
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required("ai_provider"): SelectSelector(
SelectSelectorConfig(
options=[
{"value": k, "label": v} for k, v in PROVIDERS.items()
]
)
),
}
),
)
async def async_step_configure(self, user_input=None):
"""Handle the configuration step for the selected provider."""
errors = {}
provider = self.config_data["ai_provider"]
token_field = TOKEN_FIELD_NAMES[provider]
token_label = TOKEN_LABELS[provider]
default_model = DEFAULT_MODELS[provider]
# For Alter provider, default to "Custom..." for the dropdown since model is user-provided
dropdown_default = "Custom..." if provider == "alter" else default_model
available_models = AVAILABLE_MODELS.get(provider, [default_model])
if user_input is not None:
try:
# Validate the token
token_value = user_input.get(token_field)
if not token_value:
errors[token_field] = "required"
raise InvalidApiKey
# Store the configuration data
self.config_data[token_field] = token_value
# For z.ai, store endpoint type
if provider == "zai":
endpoint_type = user_input.get("zai_endpoint", "general")
self.config_data["zai_endpoint"] = endpoint_type
# For OpenAI, store Base URL (defaults to official endpoint if unchanged)
if provider == "openai":
base_url = (user_input.get(CONF_OPENAI_BASE_URL) or "").strip()
self.config_data[CONF_OPENAI_BASE_URL] = (
base_url or "https://api.openai.com/v1"
)
# For OpenAI, move to next step to select model from dynamic list
return await self.async_step_configure_openai_models()
# For OpenAI-Compatible, store Base URL + optional API key, then move on
if provider == "openai_compatible":
base_url = (
user_input.get(CONF_OPENAI_COMPATIBLE_URL) or ""
).strip()
self.config_data[CONF_OPENAI_COMPATIBLE_URL] = base_url
api_key = (
user_input.get("openai_compatible_api_key") or ""
).strip()
self.config_data["openai_compatible_api_key"] = api_key
# Move to next step to select model from dynamic list
return await self.async_step_configure_openai_compatible_models()
# Add model configuration if provided
selected_model = user_input.get("model")
custom_model = user_input.get("custom_model")
_LOGGER.debug(
f"Config flow - Provider: {provider}, Selected model: {selected_model}, Custom model: {custom_model}"
)
# Initialize models dict if it doesn't exist
if "models" not in self.config_data:
self.config_data["models"] = {}
if custom_model and custom_model.strip():
# Use custom model if provided and not empty
self.config_data["models"][provider] = custom_model.strip()
elif selected_model and selected_model != "Custom...":
# Use selected model if it's not the "Custom..." option
self.config_data["models"][provider] = selected_model
else:
# For local_ollama, openai_compatible, alter, and zai providers, allow empty model name
if provider in (
"local_ollama",
"openai_compatible",
"alter",
"zai",
):
self.config_data["models"][provider] = ""
else:
# Fallback to default model for other providers
self.config_data["models"][provider] = default_model
return self.async_create_entry(
title=f"AI Agent HA ({PROVIDERS[provider]})",
data=self.config_data,
)
except InvalidApiKey:
errors["base"] = "invalid_api_key"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if provider == "zai":
# For z.ai provider, we need token, endpoint type, and optional model name
model_options = AVAILABLE_MODELS.get("zai", ["Custom..."])
schema_dict = {
vol.Required(token_field): TextSelector(
TextSelectorConfig(type="password")
),
vol.Optional("zai_endpoint", default="general"): SelectSelector(
SelectSelectorConfig(
options=[
{"value": "general", "label": "General Purpose"},
{"value": "coding", "label": "Coding (3× usage, 1/7 cost)"},
]
)
),
vol.Optional("model", default="glm-4.7"): SelectSelector(
SelectSelectorConfig(options=model_options)
),
vol.Optional("custom_model"): TextSelector(
TextSelectorConfig(type="text")
),
}
return self.async_show_form(
step_id="configure",
data_schema=vol.Schema(schema_dict),
errors=errors,
description_placeholders={
"token_label": token_label,
"provider": PROVIDERS[provider],
},
)
if provider == "local_ollama":
# For local_ollama provider, we need both URL and optional model name
schema_dict = {
vol.Required(CONF_LOCAL_OLLAMA_URL): TextSelector(
TextSelectorConfig(type="text")
),
}
# Add model selection
model_options = AVAILABLE_MODELS.get("local_ollama", ["Custom..."])
schema_dict[vol.Optional("model", default="Custom...")] = SelectSelector(
SelectSelectorConfig(options=model_options)
)
schema_dict[vol.Optional("custom_model")] = TextSelector(
TextSelectorConfig(type="text")
)
return self.async_show_form(
step_id="configure",
data_schema=vol.Schema(schema_dict),
errors=errors,
description_placeholders={
"token_label": "Local Ollama API URL", # nosec B105 - UI label string shown next to the URL field, not a credential
"provider": PROVIDERS[provider],
},
)
if provider == "openai_compatible":
# For openai_compatible provider, we need base URL + optional API key.
# Many local endpoints (LM Studio, vLLM) need no key; gateways like
# Open WebUI or LiteLLM require one. Models are fetched in the next step.
schema_dict = {
vol.Required(CONF_OPENAI_COMPATIBLE_URL): TextSelector(
TextSelectorConfig(type="text")
),
vol.Optional("openai_compatible_api_key", default=""): TextSelector(
TextSelectorConfig(type="password")
),
}
return self.async_show_form(
step_id="configure",
data_schema=vol.Schema(schema_dict),
errors=errors,
description_placeholders={
"token_label": "Local OpenAI-Compatible URL", # nosec B105 - UI label for config form, not a credential
"provider": PROVIDERS[provider],
},
)
if provider == "openai":
# For OpenAI provider, first step: API Key + Base URL
# Model selection happens in the next step after we fetch available models
schema_dict = {
vol.Required(token_field): TextSelector(
TextSelectorConfig(type="password")
),
vol.Optional(
CONF_OPENAI_BASE_URL,
default="https://api.openai.com/v1",
): TextSelector(TextSelectorConfig(type="text")),
}
return self.async_show_form(
step_id="configure",
data_schema=vol.Schema(schema_dict),
errors=errors,
description_placeholders={
"token_label": token_label,
"provider": PROVIDERS[provider],
},
)
# Build schema for other providers
schema_dict = {
vol.Required(token_field): TextSelector(
TextSelectorConfig(type="password")
),
}
# Add model selection if available
if available_models:
# For Gemini, fetch models dynamically
if provider == "gemini":
token_value = self.config_data.get("gemini_token")
model_list = await fetch_gemini_models(token_value)
if "Custom..." not in model_list:
model_list.insert(0, "Custom...")
model_options = model_list
else:
# Add predefined models + custom option (avoid duplicating "Custom...")
if "Custom..." in available_models:
model_options = available_models
else:
model_options = available_models + ["Custom..."]
schema_dict[vol.Optional("model", default=dropdown_default)] = (
SelectSelector(SelectSelectorConfig(options=model_options))
)
schema_dict[vol.Optional("custom_model")] = TextSelector(
TextSelectorConfig(type="text")
)
return self.async_show_form(
step_id="configure",
data_schema=vol.Schema(schema_dict),
errors=errors,
description_placeholders={
"token_label": token_label,
"provider": PROVIDERS[provider],
},
)
async def async_step_configure_openai_models(self, user_input=None):
"""Handle the OpenAI model selection step with dynamic model list."""
errors = {}
provider = "openai"
token = self.config_data.get("openai_token")
base_url = self.config_data.get(
CONF_OPENAI_BASE_URL, "https://api.openai.com/v1"
)
# Fetch available models dynamically
model_list = await fetch_openai_models(base_url, token)
# Ensure "Custom..." is always available
if "Custom..." not in model_list:
model_list.insert(0, "Custom...")
if user_input is not None:
try:
selected_model = user_input.get("model")
custom_model = user_input.get("custom_model")
# Initialize models dict if it doesn't exist
if "models" not in self.config_data:
self.config_data["models"] = {}
if custom_model and custom_model.strip():
self.config_data["models"][provider] = custom_model.strip()
elif selected_model and selected_model != "Custom...":
self.config_data["models"][provider] = selected_model
else:
self.config_data["models"][provider] = ""
return self.async_create_entry(
title=f"AI Agent HA ({PROVIDERS[provider]})",
data=self.config_data,
)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception in OpenAI model selection")
errors["base"] = "unknown"
schema_dict = {
vol.Optional("model", default="Custom..."): SelectSelector(
SelectSelectorConfig(options=model_list)
),
vol.Optional("custom_model"): TextSelector(TextSelectorConfig(type="text")),
}
return self.async_show_form(
step_id="configure_openai_models",
data_schema=vol.Schema(schema_dict),
errors=errors,
description_placeholders={
"provider": PROVIDERS[provider],
},
)
async def async_step_configure_openai_compatible_models(self, user_input=None):
"""Handle the OpenAI-Compatible model selection step with dynamic model list."""
errors = {}
provider = "openai_compatible"
base_url = self.config_data.get(CONF_OPENAI_COMPATIBLE_URL, "")
api_key = self.config_data.get("openai_compatible_api_key") or ""
# Fetch available models dynamically if the endpoint supports it
model_list = await fetch_openai_compatible_models(base_url, api_key or None)
# Ensure "Custom..." is always available
if "Custom..." not in model_list:
model_list.insert(0, "Custom...")
if user_input is not None:
try:
selected_model = user_input.get("model")
custom_model = user_input.get("custom_model")
# Initialize models dict if it doesn't exist
if "models" not in self.config_data:
self.config_data["models"] = {}
if custom_model and custom_model.strip():
self.config_data["models"][provider] = custom_model.strip()
elif selected_model and selected_model != "Custom...":
self.config_data["models"][provider] = selected_model
else:
self.config_data["models"][provider] = ""
return self.async_create_entry(
title=f"AI Agent HA ({PROVIDERS[provider]})",
data=self.config_data,
)
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
"Unexpected exception in OpenAI-Compatible model selection"
)
errors["base"] = "unknown"
schema_dict = {
vol.Optional("model", default="Custom..."): SelectSelector(
SelectSelectorConfig(options=model_list)
),
vol.Optional("custom_model"): TextSelector(TextSelectorConfig(type="text")),
}
return self.async_show_form(
step_id="configure_openai_compatible_models",
data_schema=vol.Schema(schema_dict),
errors=errors,
description_placeholders={
"provider": PROVIDERS[provider],
},
)
class InvalidApiKey(HomeAssistantError):
"""Error to indicate there is an invalid API key."""
class AiAgentHaOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options flow for AI Agent HA."""
def __init__(self):
"""Initialize options flow."""
self.options_data = {}
async def async_step_init(self, user_input=None):
"""Handle the initial options step - provider selection."""
current_provider = self.config_entry.data.get("ai_provider", DEFAULT_PROVIDER)
if user_input is not None:
# Store selected provider and move to configure step
self.options_data = {
"ai_provider": user_input["ai_provider"],
"current_provider": current_provider,
}
return await self.async_step_configure_options()
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(
"ai_provider", default=current_provider
): SelectSelector(
SelectSelectorConfig(
options=[
{"value": k, "label": v} for k, v in PROVIDERS.items()
]
)
),
}
),
description_placeholders={"current_provider": PROVIDERS[current_provider]},
)
async def async_step_configure_options(self, user_input=None):
"""Handle the configuration step for the selected provider in options."""
errors = {}
provider = self.options_data["ai_provider"]
current_provider = self.options_data["current_provider"]
token_field = TOKEN_FIELD_NAMES[provider]
token_label = TOKEN_LABELS[provider]
# Get current configuration
current_models = self.config_entry.data.get("models", {})
current_model = current_models.get(provider, DEFAULT_MODELS[provider])
# For Alter provider, if model is empty, default to "Custom..." for the dropdown
if provider == "alter" and not current_model:
current_model = "Custom..."
current_token = self.config_entry.data.get(token_field, "")
available_models = AVAILABLE_MODELS.get(provider, [DEFAULT_MODELS[provider]])
# Use current token if provider hasn't changed, otherwise empty
display_token = current_token if provider == current_provider else ""
# Determine if current model is a custom model (not in available models list)
# and prepare model dropdown and custom model field defaults
model_options = available_models
if "Custom..." not in model_options:
model_options = model_options + ["Custom..."]
# Check if current_model is a custom model (not in the available models)
# Remove "Custom..." from the check since it's the selector option, not a real model
available_models_without_custom = [
m for m in available_models if m != "Custom..."
]
is_custom_model = (
current_model
and current_model not in available_models_without_custom
and current_model != "Custom..."
)
if is_custom_model:
# Current model is a custom model - show "Custom..." in dropdown and populate custom field
model_default = "Custom..."
custom_model_default = current_model
else:
# Current model is a standard model or empty
model_default = current_model if current_model else "Custom..."
custom_model_default = ""
if user_input is not None:
try:
token_value = user_input.get(token_field)
if not token_value:
errors[token_field] = "required"
else:
# Prepare the updated configuration
updated_data = dict(self.config_entry.data)
updated_data["ai_provider"] = provider
updated_data[token_field] = token_value
# Update model configuration
selected_model = user_input.get("model")
custom_model = user_input.get("custom_model")
# For zai, update endpoint type
if provider == "zai":
endpoint_type = user_input.get("zai_endpoint", "general")
updated_data["zai_endpoint"] = endpoint_type
# For OpenAI, update Base URL (default to official if blank)
if provider == "openai":
base_url = (user_input.get(CONF_OPENAI_BASE_URL) or "").strip()
updated_data[CONF_OPENAI_BASE_URL] = (
base_url or "https://api.openai.com/v1"
)
# For OpenAI-Compatible, update the optional API key
if provider == "openai_compatible":
updated_data["openai_compatible_api_key"] = (
user_input.get("openai_compatible_api_key") or ""
).strip()
# Initialize models dict if it doesn't exist
if "models" not in updated_data:
updated_data["models"] = {}
if custom_model and custom_model.strip():
# Use custom model if provided and not empty
updated_data["models"][provider] = custom_model.strip()
elif selected_model and selected_model != "Custom...":
# Use selected model if it's not the "Custom..." option
updated_data["models"][provider] = selected_model
else:
# For local_ollama, openai_compatible, alter, and zai providers, allow empty model name
if provider in (
"local_ollama",
"openai_compatible",
"alter",
"zai",
):
updated_data["models"][provider] = ""
else:
# Ensure we keep the current model or use default for other providers
if provider not in updated_data["models"]:
updated_data["models"][provider] = DEFAULT_MODELS[
provider
]
_LOGGER.debug(
f"Options flow - Final model config for {provider}: {updated_data['models'].get(provider)}"
)
# Update the config entry
self.hass.config_entries.async_update_entry(
self.config_entry, data=updated_data
)
return self.async_create_entry(title="", data={})
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception in options flow")
errors["base"] = "unknown"
# Build schema for the selected provider in options
if provider == "zai":
current_endpoint = self.config_entry.data.get("zai_endpoint", "general")
model_options = AVAILABLE_MODELS.get("zai", ["glm-4.7"])
# Ensure "Custom..." is in model options
if "Custom..." not in model_options:
model_options = model_options + ["Custom..."]
schema_dict = {
vol.Required(token_field, default=display_token): TextSelector(
TextSelectorConfig(type="password")
),
vol.Optional("zai_endpoint", default=current_endpoint): SelectSelector(
SelectSelectorConfig(
options=[
{"value": "general", "label": "General Purpose"},
{"value": "coding", "label": "Coding (3× usage, 1/7 cost)"},
]
)
),
vol.Optional("model", default=model_default): SelectSelector(
SelectSelectorConfig(options=model_options)
),
vol.Optional(
"custom_model", default=custom_model_default
): TextSelector(TextSelectorConfig(type="text")),
}
return self.async_show_form(
step_id="configure_options",
data_schema=vol.Schema(schema_dict),
errors=errors,
description_placeholders={
"token_label": token_label,
"provider": PROVIDERS[provider],
},
)
if provider == "local_ollama":
# For local_ollama provider, we need both URL and optional model name
current_url = self.config_entry.data.get(CONF_LOCAL_OLLAMA_URL, "")
schema_dict = {
vol.Required(CONF_LOCAL_OLLAMA_URL, default=current_url): TextSelector(
TextSelectorConfig(type="text")
),
}
# Add model selection
model_options = AVAILABLE_MODELS.get("local_ollama", ["Custom..."])
# Ensure "Custom..." is in model options
if "Custom..." not in model_options:
model_options = model_options + ["Custom..."]
schema_dict[vol.Optional("model", default=model_default)] = SelectSelector(
SelectSelectorConfig(options=model_options)
)
schema_dict[vol.Optional("custom_model", default=custom_model_default)] = (
TextSelector(TextSelectorConfig(type="text"))
)
return self.async_show_form(
step_id="configure_options",
data_schema=vol.Schema(schema_dict),
errors=errors,
description_placeholders={
"token_label": "Local Ollama API URL", # nosec B105 - UI label string shown next to the URL field, not a credential
"provider": PROVIDERS[provider],
},
)
if provider == "openai_compatible":
# For openai_compatible provider, we need URL + optional API key + model
current_url = self.config_entry.data.get(CONF_OPENAI_COMPATIBLE_URL, "")
current_api_key = (
self.config_entry.data.get("openai_compatible_api_key") or ""
)
# Fetch available models dynamically if the endpoint supports it
model_list = await fetch_openai_compatible_models(
current_url, current_api_key or None
)
# Ensure "Custom..." is always available
if "Custom..." not in model_list:
model_list.insert(0, "Custom...")
schema_dict = {
vol.Required(
CONF_OPENAI_COMPATIBLE_URL, default=current_url
): TextSelector(TextSelectorConfig(type="text")),
vol.Optional(
"openai_compatible_api_key", default=current_api_key
): TextSelector(TextSelectorConfig(type="password")),
}
# Add model selection with dynamic list
schema_dict[vol.Optional("model", default=model_default)] = SelectSelector(
SelectSelectorConfig(options=model_list)
)
schema_dict[vol.Optional("custom_model", default=custom_model_default)] = (
TextSelector(TextSelectorConfig(type="text"))
)
return self.async_show_form(
step_id="configure_options",
data_schema=vol.Schema(schema_dict),
errors=errors,
description_placeholders={
"token_label": "Local OpenAI-Compatible URL", # nosec B105 - UI label for config form, not a credential
"provider": PROVIDERS[provider],
},
)
if provider == "openai":
# For OpenAI provider, we need token and optional Base URL
# Pre-fill with official endpoint if not set
current_base_url = (
self.config_entry.data.get(CONF_OPENAI_BASE_URL)
or "https://api.openai.com/v1"
)
# Fetch available models dynamically
current_token = self.config_entry.data.get("openai_token", "")
model_list = await fetch_openai_models(current_base_url, current_token)
# Ensure "Custom..." is always available
if "Custom..." not in model_list:
model_list.insert(0, "Custom...")
schema_dict = {
vol.Required(token_field, default=display_token): TextSelector(
TextSelectorConfig(type="password")
),
vol.Optional(
CONF_OPENAI_BASE_URL, default=current_base_url
): TextSelector(TextSelectorConfig(type="text")),
}
# Add model selection with dynamic list
schema_dict[vol.Optional("model", default=model_default)] = SelectSelector(
SelectSelectorConfig(options=model_list)
)
schema_dict[vol.Optional("custom_model", default=custom_model_default)] = (
TextSelector(TextSelectorConfig(type="text"))
)
return self.async_show_form(
step_id="configure_options",
data_schema=vol.Schema(schema_dict),
errors=errors,
description_placeholders={
"token_label": token_label,
"provider": PROVIDERS[provider],
},
)
# Build schema for other providers
schema_dict = {
vol.Required(token_field, default=display_token): TextSelector(
TextSelectorConfig(type="password")
),
}
# Add model selection if available
if available_models:
# For Gemini, fetch models dynamically
if provider == "gemini":
current_token = self.config_entry.data.get("gemini_token", "")
model_list = await fetch_gemini_models(current_token)
if "Custom..." not in model_list:
model_list.insert(0, "Custom...")
model_options = model_list
# model_options already has "Custom..." added above for other providers
schema_dict[vol.Optional("model", default=model_default)] = SelectSelector(
SelectSelectorConfig(options=model_options)
)
schema_dict[vol.Optional("custom_model", default=custom_model_default)] = (
TextSelector(TextSelectorConfig(type="text"))
)
return self.async_show_form(
step_id="configure_options",
data_schema=vol.Schema(schema_dict),
errors=errors,
description_placeholders={
"token_label": token_label,
"provider": PROVIDERS[provider],
},
)
-40
View File
@@ -1,40 +0,0 @@
"""Constants for the AI Agent HA integration."""
DOMAIN = "ai_agent_ha"
CONF_API_KEY = "api_key"
CONF_WEATHER_ENTITY = "weather_entity"
# AI Provider configuration keys
CONF_LLAMA_TOKEN = "llama_token" # nosec B105
CONF_OPENAI_TOKEN = "openai_token" # nosec B105
CONF_OPENAI_BASE_URL = (
"openai_base_url" # nosec B105 - configuration key, not a credential
)
CONF_GEMINI_TOKEN = "gemini_token" # nosec B105
CONF_OPENROUTER_TOKEN = "openrouter_token" # nosec B105
CONF_ANTHROPIC_TOKEN = "anthropic_token" # nosec B105
CONF_ALTER_TOKEN = "alter_token" # nosec B105
CONF_ZAI_TOKEN = "zai_token" # nosec B105
CONF_LOCAL_OLLAMA_URL = "local_ollama_url"
CONF_LOCAL_OLLAMA_MODEL = "local_ollama_model"
CONF_OPENAI_COMPATIBLE_URL = "openai_compatible_url"
CONF_LOCAL_URL = "local_url" # legacy alias for local_ollama_url
# Available AI providers
AI_PROVIDERS = [
"llama",
"openai",
"gemini",
"openrouter",
"anthropic",
"alter",
"zai",
"local_ollama",
"openai_compatible",
]
# AI Provider constants
CONF_MODELS = "models"
# Supported AI providers
DEFAULT_AI_PROVIDER = "openai"
@@ -1,390 +0,0 @@
"""
Dashboard templates and examples for AI agent to use when creating dashboards.
"""
# Basic dashboard templates for different use cases
DASHBOARD_TEMPLATES = {
"simple_lights": {
"title": "Lights Dashboard",
"url_path": "lights",
"icon": "mdi:lightbulb",
"show_in_sidebar": True,
"views": [
{
"title": "All Lights",
"cards": [
{
"type": "entities",
"title": "Living Room Lights",
"entities": [], # To be filled with actual light entities
},
{
"type": "light",
"entity": "", # To be filled with main light entity
},
],
}
],
},
"security": {
"title": "Security Dashboard",
"url_path": "security",
"icon": "mdi:security",
"show_in_sidebar": True,
"views": [
{
"title": "Security Overview",
"cards": [
{
"type": "entities",
"title": "Sensors",
"entities": [], # To be filled with sensor entities
},
{
"type": "entities",
"title": "Cameras",
"entities": [], # To be filled with camera entities
},
{
"type": "alarm-panel",
"entity": "", # To be filled with alarm panel entity
},
],
}
],
},
"climate": {
"title": "Climate Control",
"url_path": "climate",
"icon": "mdi:thermometer",
"show_in_sidebar": True,
"views": [
{
"title": "Temperature & Humidity",
"cards": [
{
"type": "thermostat",
"entity": "", # To be filled with climate.* thermostat entity (optional)
},
{
"type": "history-graph",
"title": "Temperature History",
"entities": [], # To be filled with temperature sensor entities
"hours_to_show": 24,
},
{
"type": "entities",
"title": "Temperature Sensors",
"entities": [], # To be filled with temperature sensor entities (device_class: temperature)
},
{
"type": "history-graph",
"title": "Humidity History",
"entities": [], # To be filled with humidity sensor entities
"hours_to_show": 24,
},
{
"type": "entities",
"title": "Humidity Sensors",
"entities": [], # To be filled with humidity sensor entities (device_class: humidity)
},
{
"type": "weather-forecast",
"entity": "", # To be filled with weather entity (optional)
},
],
}
],
},
"media": {
"title": "Media Center",
"url_path": "media",
"icon": "mdi:play",
"show_in_sidebar": True,
"views": [
{
"title": "Media Players",
"cards": [
{
"type": "media-control",
"entity": "", # To be filled with media player entity
},
{
"type": "entities",
"title": "All Media Players",
"entities": [], # To be filled with media player entities
},
],
}
],
},
"energy": {
"title": "Energy Monitoring",
"url_path": "energy",
"icon": "mdi:lightning-bolt",
"show_in_sidebar": True,
"views": [
{
"title": "Energy Usage",
"cards": [
{"type": "energy-distribution", "title": "Energy Distribution"},
{
"type": "entities",
"title": "Power Sensors",
"entities": [], # To be filled with power sensor entities
},
{
"type": "history-graph",
"title": "Power Usage",
"entities": [], # To be filled with power entities
},
],
}
],
},
}
# Card type examples and their typical use cases
CARD_EXAMPLES = {
"entities": {
"description": "Shows a list of entities with their states",
"example": {
"type": "entities",
"title": "Living Room",
"entities": [
"light.living_room_main",
"switch.living_room_fan",
"sensor.living_room_temperature",
],
},
},
"glance": {
"description": "Shows entities in a compact grid format",
"example": {
"type": "glance",
"title": "Quick Overview",
"entities": [
"binary_sensor.front_door",
"binary_sensor.back_door",
"binary_sensor.garage_door",
],
},
},
"thermostat": {
"description": "Controls and displays thermostat information",
"example": {"type": "thermostat", "entity": "climate.main_thermostat"},
},
"weather-forecast": {
"description": "Shows weather information and forecast",
"example": {
"type": "weather-forecast",
"entity": "weather.home",
"name": "Weather",
},
},
"media-control": {
"description": "Controls media players with full interface",
"example": {"type": "media-control", "entity": "media_player.living_room_tv"},
},
"light": {
"description": "Dedicated light control card",
"example": {"type": "light", "entity": "light.living_room_main"},
},
"alarm-panel": {
"description": "Security alarm panel interface",
"example": {"type": "alarm-panel", "entity": "alarm_control_panel.home_alarm"},
},
"picture-entity": {
"description": "Shows entity state with a background image",
"example": {
"type": "picture-entity",
"entity": "light.living_room",
"image": "/local/living_room.jpg",
},
},
"history-graph": {
"description": "Shows historical data as a graph",
"example": {
"type": "history-graph",
"title": "Temperature History",
"entities": ["sensor.temperature_indoor", "sensor.temperature_outdoor"],
"hours_to_show": 24,
},
},
"gauge": {
"description": "Shows a single entity value as a gauge",
"example": {
"type": "gauge",
"entity": "sensor.cpu_temperature",
"min": 0,
"max": 100,
"name": "CPU Temperature",
},
},
}
# Predefined color schemes and icons
COMMON_ICONS = {
"lights": "mdi:lightbulb",
"security": "mdi:security",
"climate": "mdi:thermometer",
"energy": "mdi:lightning-bolt",
"media": "mdi:play",
"kitchen": "mdi:chef-hat",
"bedroom": "mdi:bed",
"bathroom": "mdi:shower",
"living_room": "mdi:sofa",
"garage": "mdi:garage",
"garden": "mdi:flower",
"office": "mdi:desk",
"basement": "mdi:stairs-down",
"attic": "mdi:stairs-up",
}
def get_template_for_entities(entities, dashboard_type="general"):
"""Generate a dashboard template based on available entities."""
template = {
"title": f"{dashboard_type.title()} Dashboard",
"url_path": dashboard_type.lower().replace(" ", "-"),
"icon": COMMON_ICONS.get(dashboard_type, "mdi:view-dashboard"),
"show_in_sidebar": True,
"views": [],
}
# Group entities by domain and by device_class for sensors
entity_groups = {}
sensor_by_device_class = {}
for entity in entities:
if isinstance(entity, dict) and "entity_id" in entity:
entity_id = entity["entity_id"]
# Get device_class from attributes if available
device_class = entity.get("attributes", {}).get("device_class")
else:
entity_id = str(entity)
device_class = None
domain = entity_id.split(".")[0]
if domain not in entity_groups:
entity_groups[domain] = []
entity_groups[domain].append(entity_id)
# Categorize sensors by device_class
if domain == "sensor" and device_class:
if device_class not in sensor_by_device_class:
sensor_by_device_class[device_class] = []
sensor_by_device_class[device_class].append(entity_id)
# Create view with cards for each domain
view_cards = []
# Lights
if "light" in entity_groups:
view_cards.append(
{"type": "entities", "title": "Lights", "entities": entity_groups["light"]}
)
# Climate - handle both climate entities and temperature/humidity sensors
if "climate" in entity_groups:
for climate_entity in entity_groups["climate"]:
view_cards.append({"type": "thermostat", "entity": climate_entity})
# Temperature sensors - create history graphs for climate dashboards
if "temperature" in sensor_by_device_class:
temp_sensors = sensor_by_device_class["temperature"]
# Add history graph for temperature visualization
view_cards.append(
{
"type": "history-graph",
"title": "Temperature History",
"entities": temp_sensors,
"hours_to_show": 24,
}
)
# Add entity card for current values
view_cards.append(
{
"type": "entities",
"title": "Temperature Sensors",
"entities": temp_sensors,
}
)
# Humidity sensors - create gauge cards for climate dashboards
if "humidity" in sensor_by_device_class:
humidity_sensors = sensor_by_device_class["humidity"]
# Add history graph for humidity visualization
view_cards.append(
{
"type": "history-graph",
"title": "Humidity History",
"entities": humidity_sensors,
"hours_to_show": 24,
}
)
# Add entity card for current values
view_cards.append(
{
"type": "entities",
"title": "Humidity Sensors",
"entities": humidity_sensors,
}
)
# Media players
if "media_player" in entity_groups:
for media_entity in entity_groups["media_player"]:
view_cards.append({"type": "media-control", "entity": media_entity})
# Security entities
if "binary_sensor" in entity_groups:
view_cards.append(
{
"type": "entities",
"title": "Sensors",
"entities": entity_groups["binary_sensor"],
}
)
if "alarm_control_panel" in entity_groups:
for alarm_entity in entity_groups["alarm_control_panel"]:
view_cards.append({"type": "alarm-panel", "entity": alarm_entity})
# Other Sensors (not temperature or humidity)
if "sensor" in entity_groups:
# Filter out temperature and humidity sensors that we already handled
other_sensors = [
s
for s in entity_groups["sensor"]
if s not in sensor_by_device_class.get("temperature", [])
and s not in sensor_by_device_class.get("humidity", [])
]
if other_sensors:
view_cards.append(
{
"type": "entities",
"title": "Other Sensors",
"entities": other_sensors[:10], # Limit to first 10
}
)
# Switches
if "switch" in entity_groups:
view_cards.append(
{
"type": "entities",
"title": "Switches",
"entities": entity_groups["switch"],
}
)
# Weather
if "weather" in entity_groups:
view_cards.append(
{"type": "weather-forecast", "entity": entity_groups["weather"][0]}
)
template["views"] = [{"title": "Overview", "cards": view_cards}]
return template
@@ -1,8 +0,0 @@
panel_custom:
- name: AI Agent HA
sidebar_title: AI Agent HA
sidebar_icon: mdi:robot
url_path: ai_agent_ha
module_url: /local/ai_agent_ha/frontend/ai_agent_ha-panel.js
trust_external: true
require_admin: false
File diff suppressed because it is too large Load Diff
@@ -1,24 +0,0 @@
{
"domain": "ai_agent_ha",
"name": "AI Agent HA",
"after_dependencies": [
"history",
"recorder",
"lovelace"
],
"codeowners": [
"@sbenodiz"
],
"config_flow": true,
"dependencies": [
"http"
],
"documentation": "https://github.com/sbenodiz/ai_agent_ha",
"integration_type": "service",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/sbenodiz/ai_agent_ha/issues",
"requirements": [
"aiohttp>=3.8.0"
],
"version": "1.14.1"
}
@@ -1,98 +0,0 @@
query:
name: "Query AI Agent with Home Assistant context"
description: "Run a custom AI prompt against your Home Assistant state dump."
fields:
prompt:
description: "The question or instruction to send to AI model."
example: "Turn on all the lights in the living room"
debug:
description: "Include a debug trace of the HA↔AI conversation (true/false)."
example: true
default: false
provider:
description: "The AI provider to use (openai, llama, gemini, openrouter, anthropic, alter, zai, local)"
example: "openai"
default: "openai"
selector:
select:
options:
- "openai"
- "llama"
- "gemini"
- "openrouter"
- "anthropic"
- "alter"
- "zai"
- "local"
create_dashboard:
name: "Create Dashboard via AI Agent"
description: "Create a new Home Assistant dashboard using AI assistance."
fields:
dashboard_config:
description: "The dashboard configuration as a JSON object."
example: '{"title": "My Dashboard", "url_path": "my-dashboard", "views": []}'
provider:
description: "The AI provider to use (openai, llama, gemini, openrouter, anthropic, alter, zai, local)"
example: "openai"
default: "openai"
selector:
select:
options:
- "openai"
- "llama"
- "gemini"
- "openrouter"
- "anthropic"
- "alter"
- "zai"
- "local"
create_automation:
name: "Create Automation via AI Agent"
description: "Create a new Home Assistant automation using AI assistance."
fields:
automation:
description: "The automation configuration as a JSON object."
example: '{"alias": "Turn off lights at 9 PM", "trigger": [{"platform": "time", "at": "21:00:00"}], "action": [{"service": "light.turn_off", "target": {"entity_id": "light.living_room"}}]}'
provider:
description: "The AI provider to use (openai, llama, gemini, openrouter, anthropic, alter, zai, local)"
example: "openai"
default: "openai"
selector:
select:
options:
- "openai"
- "llama"
- "gemini"
- "openrouter"
- "anthropic"
- "alter"
- "zai"
- "local"
update_dashboard:
name: "Update Dashboard via AI Agent"
description: "Update an existing Home Assistant dashboard using AI assistance."
fields:
dashboard_url:
description: "The URL path of dashboard to update."
example: "my-dashboard"
dashboard_config:
description: "The updated dashboard configuration as a JSON object."
example: '{"title": "Updated Dashboard", "views": []}'
provider:
description: "The AI provider to use (openai, llama, gemini, openrouter, anthropic, alter, zai, local)"
example: "openai"
default: "openai"
selector:
select:
options:
- "openai"
- "llama"
- "gemini"
- "openrouter"
- "anthropic"
- "alter"
- "zai"
- "local"
@@ -1,91 +0,0 @@
{
"config": {
"step": {
"user": {
"title": "Choose AI Provider",
"description": "Select your AI provider",
"data": {
"ai_provider": "AI Provider"
},
"data_description": {
"ai_provider": "Choose your preferred AI provider"
}
},
"configure": {
"title": "Configure {provider}",
"description": "Enter your {token_label} and optionally select a model",
"data": {
"llama_token": "Llama API Token",
"openai_token": "OpenAI API Key",
"gemini_token": "Google Gemini API Key",
"openrouter_token": "OpenRouter API Key",
"anthropic_token": "Anthropic API Key",
"alter_token": "Alter API Key",
"model": "Model (Optional)",
"custom_model": "Custom Model (Optional)"
},
"data_description": {
"llama_token": "Enter your Llama API token",
"openai_token": "Enter your OpenAI API key",
"gemini_token": "Enter your Google Gemini API key",
"openrouter_token": "Enter your OpenRouter API key",
"anthropic_token": "Enter your Anthropic API key",
"alter_token": "Enter your Alter API key",
"model": "Choose a predefined model or select 'Custom...' to enter your own",
"custom_model": "Enter a custom model name (only used if 'Custom...' is selected above)"
}
}
},
"error": {
"invalid_api_key": "Invalid API key format",
"unknown": "Unknown error occurred",
"llama_token": "Llama API token is required",
"openai_token": "OpenAI API key is required",
"gemini_token": "Google Gemini API key is required",
"openrouter_token": "OpenRouter API key is required",
"anthropic_token": "Anthropic API key is required",
"alter_token": "Alter API key is required"
},
"abort": {
"already_configured": "AI Agent HA is already configured"
}
},
"options": {
"step": {
"init": {
"title": "AI Provider Settings",
"description": "Current provider: {current_provider}. Select a provider to configure",
"data": {
"ai_provider": "AI Provider"
},
"data_description": {
"ai_provider": "Choose your preferred AI provider"
}
},
"configure_options": {
"title": "Configure {provider}",
"description": "Update your {token_label} and model settings",
"data": {
"llama_token": "Llama API Token",
"openai_token": "OpenAI API Key",
"gemini_token": "Google Gemini API Key",
"openrouter_token": "OpenRouter API Key",
"anthropic_token": "Anthropic API Key",
"alter_token": "Alter API Key",
"model": "Model",
"custom_model": "Custom Model (Optional)"
},
"data_description": {
"llama_token": "Enter your Llama API token",
"openai_token": "Enter your OpenAI API key",
"gemini_token": "Enter your Google Gemini API key",
"openrouter_token": "Enter your OpenRouter API key",
"anthropic_token": "Enter your Anthropic API key",
"alter_token": "Enter your Alter API key",
"model": "Choose a model or select 'Custom...' to enter your own",
"custom_model": "Enter a custom model name (only used if 'Custom...' is selected above)"
}
}
}
}
}
@@ -1,81 +0,0 @@
{
"config": {
"step": {
"user": {
"title": "Agent d'IA HA - Seleccionar proveïdor",
"description": "Tria el teu proveïdor d'IA",
"data": {
"ai_provider": "Proveïdor d'IA"
},
"data_description": {
"ai_provider": "Selecciona el teu proveïdor d'IA preferit"
}
},
"configure": {
"title": "Agent d'IA HA - Configurar {provider}",
"description": "Configura les teves credencials d'API i el model de {provider}",
"data": {
"llama_token": "Token de l'API de Llama",
"openai_token": "Clau de l'API d'OpenAI",
"gemini_token": "Clau de l'API de Google Gemini",
"openrouter_token": "Clau de l'API d'OpenRouter",
"model": "Model (Opcional)",
"custom_model": "Nom del model personalitzat (Opcional)"
},
"data_description": {
"llama_token": "Introdueix el teu token de l'API de Llama",
"openai_token": "Introdueix la teva clau de l'API d'OpenAI",
"gemini_token": "Introdueix la teva clau de l'API de Google Gemini",
"openrouter_token": "Introdueix la teva clau de l'API d'OpenRouter",
"model": "Tria un model predefinit o selecciona 'Personalitzat...' per utilitzar un model personalitzat",
"custom_model": "Introdueix un nom de model personalitzat (anul·la la selecció desplegable anterior)"
}
}
},
"error": {
"invalid_api_key": "Clau o token d'API no vàlid",
"llama_token": "Es requereix el token de l'API de Llama",
"openai_token": "Es requereix la clau de l'API d'OpenAI",
"gemini_token": "Es requereix la clau de l'API de Google Gemini",
"openrouter_token": "Es requereix la clau de l'API d'OpenRouter",
"unknown": "S'ha produït un error inesperat"
},
"abort": {
"already_configured": "L'Agent d'IA HA ja està configurat"
}
},
"options": {
"step": {
"init": {
"title": "Agent d'IA HA - Seleccionar proveïdor",
"description": "Tria el teu proveïdor d'IA (Actual: {current_provider})",
"data": {
"ai_provider": "Proveïdor d'IA"
},
"data_description": {
"ai_provider": "Selecciona el teu proveïdor d'IA preferit"
}
},
"configure_options": {
"title": "Agent d'IA HA - Configurar {provider}",
"description": "Actualitza les teves credencials d'API i el model de {provider}",
"data": {
"llama_token": "Token de l'API de Llama",
"openai_token": "Clau de l'API d'OpenAI",
"gemini_token": "Clau de l'API de Google Gemini",
"openrouter_token": "Clau de l'API d'OpenRouter",
"model": "Model (Opcional)",
"custom_model": "Nom del model personalitzat (Opcional)"
},
"data_description": {
"llama_token": "Introdueix el teu token de l'API de Llama",
"openai_token": "Introdueix la teva clau de l'API d'OpenAI",
"gemini_token": "Introdueix la teva clau de l'API de Google Gemini",
"openrouter_token": "Introdueix la teva clau de l'API d'OpenRouter",
"model": "Tria un model predefinit o selecciona 'Personalitzat...' per utilitzar un model personalitzat",
"custom_model": "Introdueix un nom de model personalitzat (anul·la la selecció desplegable anterior)"
}
}
}
}
}
@@ -1,86 +0,0 @@
{
"config": {
"step": {
"user": {
"title": "KI-Anbieter wählen",
"description": "Wähle deinen KI-Anbieter aus",
"data": {
"ai_provider": "KI-Anbieter"
},
"data_description": {
"ai_provider": "Wähle deinen bevorzugten KI-Anbieter"
}
},
"configure": {
"title": "{provider} konfigurieren",
"description": "Gib deinen {token_label} ein und wähle optional ein Model",
"data": {
"llama_token": "Llama API Token",
"openai_token": "OpenAI API Key",
"gemini_token": "Google Gemini API Key",
"openrouter_token": "OpenRouter API Key",
"anthropic_token": "Anthropic API Key",
"model": "Model (Optional)",
"custom_model": "Custom Model (Optional)"
},
"data_description": {
"llama_token": "Gib deinen Llama API Token ein",
"openai_token": "Gib deinen OpenAI API Key ein",
"gemini_token": "Gib deinen Google Gemini API Key ein",
"openrouter_token": "Gib deinen OpenRouter API Key ein",
"anthropic_token": "Gib deinen Anthropic API Key ein",
"model": "Wähle ein vordefiniertes Model oder 'Custom...' zum manuellen Eintrag",
"custom_model": "Gib einen benutzerdefinierten Model-Namen ein (nur relevant bei Auswahl von 'Custom...')"
}
}
},
"error": {
"invalid_api_key": "Ungültiges API-Key-Format",
"unknown": "Unbekannter Fehler aufgetreten",
"llama_token": "Llama API Token ist erforderlich",
"openai_token": "OpenAI API Key ist erforderlich",
"gemini_token": "Google Gemini API Key ist erforderlich",
"openrouter_token": "OpenRouter API Key ist erforderlich",
"anthropic_token": "Anthropic API Key ist erforderlich"
},
"abort": {
"already_configured": "AI Agent HA ist bereits konfiguriert"
}
},
"options": {
"step": {
"init": {
"title": "Einstellungen für KI-Anbieter",
"description": "Aktueller Anbieter: {current_provider}. Wähle einen Anbieter zur Konfiguration",
"data": {
"ai_provider": "KI-Anbieter"
},
"data_description": {
"ai_provider": "Wähle deinen bevorzugten KI-Anbieter"
}
},
"configure_options": {
"title": "{provider} konfigurieren",
"description": "Aktualisiere deinen {token_label} und Model-Einstellungen",
"data": {
"llama_token": "Llama API Token",
"openai_token": "OpenAI API Key",
"gemini_token": "Google Gemini API Key",
"openrouter_token": "OpenRouter API Key",
"anthropic_token": "Anthropic API Key",
"model": "Model",
"custom_model": "Custom Model (Optional)"
},
"data_description": {
"llama_token": "Gib deinen Llama API Token ein",
"openai_token": "Gib deinen OpenAI API Key ein",
"gemini_token": "Gib deinen Google Gemini API Key ein",
"openrouter_token": "Gib deinen OpenRouter API Key ein",
"anthropic_token": "Gib deinen Anthropic API Key ein",
"model": "Wähle ein Model oder 'Custom...' zum manuellen Eintrag",
"custom_model": "Gib einen benutzerdefinierten Model-Namen ein (nur relevant bei Auswahl von 'Custom...')"
}
}
}
}
}
@@ -1,91 +0,0 @@
{
"config": {
"step": {
"user": {
"title": "Choose AI Provider",
"description": "Select your AI provider",
"data": {
"ai_provider": "AI Provider"
},
"data_description": {
"ai_provider": "Choose your preferred AI provider"
}
},
"configure": {
"title": "Configure {provider}",
"description": "Enter your {token_label} and optionally select a model",
"data": {
"llama_token": "Llama API Token",
"openai_token": "OpenAI API Key",
"gemini_token": "Google Gemini API Key",
"openrouter_token": "OpenRouter API Key",
"anthropic_token": "Anthropic API Key",
"alter_token": "Alter API Key",
"model": "Model (Optional)",
"custom_model": "Custom Model (Optional)"
},
"data_description": {
"llama_token": "Enter your Llama API token",
"openai_token": "Enter your OpenAI API key",
"gemini_token": "Enter your Google Gemini API key",
"openrouter_token": "Enter your OpenRouter API key",
"anthropic_token": "Enter your Anthropic API key",
"alter_token": "Enter your Alter API key",
"model": "Choose a predefined model or select 'Custom...' to enter your own",
"custom_model": "Enter a custom model name (only used if 'Custom...' is selected above)"
}
}
},
"error": {
"invalid_api_key": "Invalid API key format",
"unknown": "Unknown error occurred",
"llama_token": "Llama API token is required",
"openai_token": "OpenAI API key is required",
"gemini_token": "Google Gemini API key is required",
"openrouter_token": "OpenRouter API key is required",
"anthropic_token": "Anthropic API key is required",
"alter_token": "Alter API key is required"
},
"abort": {
"already_configured": "AI Agent HA is already configured"
}
},
"options": {
"step": {
"init": {
"title": "AI Provider Settings",
"description": "Current provider: {current_provider}. Select a provider to configure",
"data": {
"ai_provider": "AI Provider"
},
"data_description": {
"ai_provider": "Choose your preferred AI provider"
}
},
"configure_options": {
"title": "Configure {provider}",
"description": "Update your {token_label} and model settings",
"data": {
"llama_token": "Llama API Token",
"openai_token": "OpenAI API Key",
"gemini_token": "Google Gemini API Key",
"openrouter_token": "OpenRouter API Key",
"anthropic_token": "Anthropic API Key",
"alter_token": "Alter API Key",
"model": "Model",
"custom_model": "Custom Model (Optional)"
},
"data_description": {
"llama_token": "Enter your Llama API token",
"openai_token": "Enter your OpenAI API key",
"gemini_token": "Enter your Google Gemini API key",
"openrouter_token": "Enter your OpenRouter API key",
"anthropic_token": "Enter your Anthropic API key",
"alter_token": "Enter your Alter API key",
"model": "Choose a model or select 'Custom...' to enter your own",
"custom_model": "Enter a custom model name (only used if 'Custom...' is selected above)"
}
}
}
}
}
@@ -1,81 +0,0 @@
{
"config": {
"step": {
"user": {
"title": "AI Agent HA - Select Provider",
"description": "Choose your AI provider",
"data": {
"ai_provider": "AI Provider"
},
"data_description": {
"ai_provider": "Select your preferred AI provider"
}
},
"configure": {
"title": "AI Agent HA - Configure {provider}",
"description": "Configure your {provider} API credentials and model",
"data": {
"llama_token": "Llama API Token",
"openai_token": "OpenAI API Key",
"gemini_token": "Google Gemini API Key",
"openrouter_token": "OpenRouter API Key",
"model": "Model (Optional)",
"custom_model": "Custom Model Name (Optional)"
},
"data_description": {
"llama_token": "Enter your Llama API token",
"openai_token": "Enter your OpenAI API key",
"gemini_token": "Enter your Google Gemini API key",
"openrouter_token": "Enter your OpenRouter API key",
"model": "Choose a predefined model or select 'Custom...' to use a custom model",
"custom_model": "Enter a custom model name (overrides the dropdown selection above)"
}
}
},
"error": {
"invalid_api_key": "Invalid API key or token",
"llama_token": "Llama API token is required",
"openai_token": "OpenAI API key is required",
"gemini_token": "Google Gemini API key is required",
"openrouter_token": "OpenRouter API key is required",
"unknown": "Unexpected error occurred"
},
"abort": {
"already_configured": "AI Agent HA is already configured"
}
},
"options": {
"step": {
"init": {
"title": "AI Agent HA - Select Provider",
"description": "Choose your AI provider (Current: {current_provider})",
"data": {
"ai_provider": "AI Provider"
},
"data_description": {
"ai_provider": "Select your preferred AI provider"
}
},
"configure_options": {
"title": "AI Agent HA - Configure {provider}",
"description": "Update your {provider} API credentials and model",
"data": {
"llama_token": "Llama API Token",
"openai_token": "OpenAI API Key",
"gemini_token": "Google Gemini API Key",
"openrouter_token": "OpenRouter API Key",
"model": "Model (Optional)",
"custom_model": "Custom Model Name (Optional)"
},
"data_description": {
"llama_token": "Enter your Llama API token",
"openai_token": "Enter your OpenAI API key",
"gemini_token": "Enter your Google Gemini API key",
"openrouter_token": "Enter your OpenRouter API key",
"model": "Choose a predefined model or select 'Custom...' to use a custom model",
"custom_model": "Enter a custom model name (overrides the dropdown selection above)"
}
}
}
}
}
@@ -1,81 +0,0 @@
{
"config": {
"step": {
"user": {
"title": "Agente de IA HA - Seleccionar proveedor",
"description": "Elige tu proveedor de IA",
"data": {
"ai_provider": "Proveedor de IA"
},
"data_description": {
"ai_provider": "Selecciona tu proveedor de IA preferido"
}
},
"configure": {
"title": "Agente de IA HA - Configurar {provider}",
"description": "Configura tus credenciales de API y modelo de {provider}",
"data": {
"llama_token": "Token de la API de Llama",
"openai_token": "Clave de la API de OpenAI",
"gemini_token": "Clave de la API de Google Gemini",
"openrouter_token": "Clave de la API de OpenRouter",
"model": "Modelo (Opcional)",
"custom_model": "Nombre de modelo personalizado (Opcional)"
},
"data_description": {
"llama_token": "Introduce tu token de la API de Llama",
"openai_token": "Introduce tu clave de la API de OpenAI",
"gemini_token": "Introduce tu clave de la API de Google Gemini",
"openrouter_token": "Introduce tu clave de la API de OpenRouter",
"model": "Elige un modelo predefinido o selecciona 'Personalizado...' para usar un modelo personalizado",
"custom_model": "Introduce un nombre de modelo personalizado (anula la selección desplegable anterior)"
}
}
},
"error": {
"invalid_api_key": "Clave o token de API no válido",
"llama_token": "Se requiere el token de la API de Llama",
"openai_token": "Se requiere la clave de la API de OpenAI",
"gemini_token": "Se requiere la clave de la API de Google Gemini",
"openrouter_token": "Se requiere la clave de la API de OpenRouter",
"unknown": "Ha ocurrido un error inesperado"
},
"abort": {
"already_configured": "El Agente de IA HA ya está configurado"
}
},
"options": {
"step": {
"init": {
"title": "Agente de IA HA - Seleccionar proveedor",
"description": "Elige tu proveedor de IA (Actual: {current_provider})",
"data": {
"ai_provider": "Proveedor de IA"
},
"data_description": {
"ai_provider": "Selecciona tu proveedor de IA preferido"
}
},
"configure_options": {
"title": "Agente de IA HA - Configurar {provider}",
"description": "Actualiza tus credenciales de API y modelo de {provider}",
"data": {
"llama_token": "Token de la API de Llama",
"openai_token": "Clave de la API de OpenAI",
"gemini_token": "Clave de la API de Google Gemini",
"openrouter_token": "Clave de la API de OpenRouter",
"model": "Modelo (Opcional)",
"custom_model": "Nombre de modelo personalizado (Opcional)"
},
"data_description": {
"llama_token": "Introduce tu token de la API de Llama",
"openai_token": "Introduce tu clave de la API de OpenAI",
"gemini_token": "Introduce tu clave de la API de Google Gemini",
"openrouter_token": "Introduce tu clave de la API de OpenRouter",
"model": "Elige un modelo predefinido o selecciona 'Personalizado...' para usar un modelo personalizado",
"custom_model": "Introduce un nombre de modelo personalizado (anula la selección desplegable anterior)"
}
}
}
}
}
@@ -54,6 +54,19 @@ from .const import (
DEFAULT_ICLOUD_IMAGE_SIZE,
ICLOUD_IMAGE_FULL,
ICLOUD_IMAGE_PREVIEW,
CONF_SYNOLOGY_URL,
CONF_SYNOLOGY_USERNAME,
CONF_SYNOLOGY_PASSWORD,
CONF_SYNOLOGY_DEVICE_ID,
CONF_SYNOLOGY_SPACE,
CONF_SYNOLOGY_ALBUM_ID,
CONF_SYNOLOGY_IMAGE_SIZE,
DEFAULT_SYNOLOGY_IMAGE_SIZE,
SYNOLOGY_SPACE_PERSONAL,
SYNOLOGY_SPACE_SHARED,
SYNOLOGY_IMAGE_SMALL,
SYNOLOGY_IMAGE_MEDIUM,
SYNOLOGY_IMAGE_LARGE,
DEFAULT_REVERSE_GEOCODE,
PROVIDER_GOOGLE_SHARED,
PROVIDER_LOCAL_FOLDER,
@@ -61,6 +74,7 @@ from .const import (
PROVIDER_IMMICH,
PROVIDER_PHOTOPRISM,
PROVIDER_ICLOUD,
PROVIDER_SYNOLOGY,
DEFAULT_RECURSIVE,
)
@@ -108,6 +122,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._pp_password: str | None = None
self._pp_albums: dict[str, str] = {}
self._pp_people: dict[str, str] = {}
# Synology flow state carried between steps.
self._syn_url: str | None = None
self._syn_username: str | None = None
self._syn_password: str | None = None
self._syn_device_id: str | None = None
self._syn_space: str = SYNOLOGY_SPACE_PERSONAL
self._syn_albums: dict[str, str] = {}
@staticmethod
@callback
@@ -143,6 +164,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_photoprism()
if self._provider == PROVIDER_ICLOUD:
return await self.async_step_icloud()
if self._provider == PROVIDER_SYNOLOGY:
return await self.async_step_synology()
return await self.async_step_google_shared()
schema = vol.Schema(
@@ -153,6 +176,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
PROVIDER_IMMICH: "Immich (direct API, full metadata)",
PROVIDER_PHOTOPRISM: "PhotoPrism (direct API, full metadata)",
PROVIDER_ICLOUD: "iCloud Shared Album",
PROVIDER_SYNOLOGY: "Synology Photos (direct API, full metadata)",
PROVIDER_MEDIA_SOURCE: "Media Source (any source, no metadata)",
})
}
@@ -651,6 +675,140 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="icloud", data_schema=schema, errors=errors
)
async def async_step_synology(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Collect the Synology URL + credentials (and optional 2FA code)."""
errors: dict[str, str] = {}
if user_input is not None:
url = user_input[CONF_SYNOLOGY_URL].strip()
username = user_input[CONF_SYNOLOGY_USERNAME].strip()
password = user_input.get(CONF_SYNOLOGY_PASSWORD) or ""
space = user_input.get(CONF_SYNOLOGY_SPACE, SYNOLOGY_SPACE_PERSONAL)
otp = (user_input.get("otp_code") or "").strip()
from . import synology as syn_api
client = syn_api.SynologyClient(
self.hass,
url,
username=username,
password=password,
space=space,
)
try:
await client.async_login(otp_code=otp or None)
albums = await client.async_list_albums()
except syn_api.SynologyOtpRequired:
errors["otp_code"] = "synology_otp_required"
except Exception: # noqa: BLE001 - any failure means bad URL/creds
errors["base"] = "synology_cannot_connect"
else:
self._syn_url = client.base_url
self._syn_username = username
self._syn_password = password
self._syn_space = space
# A trusted-device token is captured only on the OTP login;
# store it so future logins skip the 2FA prompt.
self._syn_device_id = client.captured_device_id
self._syn_albums = {
str(a["id"]): (a.get("name") or str(a["id"]))
for a in albums
if a.get("id") is not None
}
await client.async_logout()
return await self.async_step_synology_select()
schema = vol.Schema(
{
vol.Required(CONF_SYNOLOGY_URL): str,
vol.Required(CONF_SYNOLOGY_USERNAME): str,
vol.Required(CONF_SYNOLOGY_PASSWORD): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
),
vol.Required(
CONF_SYNOLOGY_SPACE, default=SYNOLOGY_SPACE_PERSONAL
): vol.In(
{
SYNOLOGY_SPACE_PERSONAL: "Personal (My Photos)",
SYNOLOGY_SPACE_SHARED: "Shared Space",
}
),
vol.Optional("otp_code"): str,
}
)
return self.async_show_form(
step_id="synology", data_schema=schema, errors=errors
)
async def async_step_synology_select(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Pick a Synology album (or the whole space) and image size."""
errors: dict[str, str] = {}
if user_input is not None:
name = user_input[CONF_ALBUM_NAME].strip()
size = user_input.get(
CONF_SYNOLOGY_IMAGE_SIZE, DEFAULT_SYNOLOGY_IMAGE_SIZE
)
album_id = user_input.get("album")
if album_id in (None, "", "__all__") or album_id not in self._syn_albums:
album_id = ""
unique = (
f"{DOMAIN}:{PROVIDER_SYNOLOGY}:{self._syn_url}:"
f"{self._syn_space}:{album_id or 'all'}"
)
await self.async_set_unique_id(unique)
self._abort_if_unique_id_configured()
data = {
CONF_PROVIDER: PROVIDER_SYNOLOGY,
CONF_SYNOLOGY_URL: self._syn_url,
CONF_SYNOLOGY_USERNAME: self._syn_username,
CONF_SYNOLOGY_PASSWORD: self._syn_password,
CONF_SYNOLOGY_SPACE: self._syn_space,
CONF_SYNOLOGY_IMAGE_SIZE: size,
CONF_ALBUM_NAME: name,
}
if album_id:
data[CONF_SYNOLOGY_ALBUM_ID] = album_id
if self._syn_device_id:
data[CONF_SYNOLOGY_DEVICE_ID] = self._syn_device_id
return self.async_create_entry(title=name, data=data)
album_options = [
selector.SelectOptionDict(value="__all__", label="All photos in this space")
] + [
selector.SelectOptionDict(value=aid, label=aname)
for aid, aname in self._syn_albums.items()
]
schema = vol.Schema(
{
vol.Required(CONF_ALBUM_NAME): str,
vol.Optional("album", default="__all__"): selector.SelectSelector(
selector.SelectSelectorConfig(
options=album_options,
mode=selector.SelectSelectorMode.DROPDOWN,
custom_value=False,
)
),
vol.Optional(
CONF_SYNOLOGY_IMAGE_SIZE, default=DEFAULT_SYNOLOGY_IMAGE_SIZE
): vol.In(
{
SYNOLOGY_IMAGE_LARGE: "Large (best for slideshow)",
SYNOLOGY_IMAGE_MEDIUM: "Medium",
SYNOLOGY_IMAGE_SMALL: "Small (thumbnail, fastest)",
}
),
}
)
return self.async_show_form(
step_id="synology_select", data_schema=schema, errors=errors
)
class LocalFolderOptionsFlow(config_entries.OptionsFlow):
"""Options for local-folder entries.
@@ -57,6 +57,34 @@ PROVIDER_MEDIA_SOURCE = "media_source"
PROVIDER_IMMICH = "immich"
PROVIDER_PHOTOPRISM = "photoprism"
PROVIDER_ICLOUD = "icloud"
PROVIDER_SYNOLOGY = "synology"
# Synology Photos (direct API) provider. Talks to a DSM Photos package over its
# entry.cgi web API. The account password is stored so the coordinator can
# re-authenticate when the session id expires; accounts with 2FA are handled by
# capturing a trusted-device token during setup (see synology.py).
CONF_SYNOLOGY_URL = "synology_url"
CONF_SYNOLOGY_USERNAME = "synology_username"
CONF_SYNOLOGY_PASSWORD = "synology_password"
CONF_SYNOLOGY_DEVICE_ID = "synology_device_id"
CONF_SYNOLOGY_SPACE = "synology_space"
CONF_SYNOLOGY_ALBUM_ID = "synology_album_id"
CONF_SYNOLOGY_IMAGE_SIZE = "synology_image_size"
# Personal ("My Photos") vs shared ("Shared Space") library.
SYNOLOGY_SPACE_PERSONAL = "personal"
SYNOLOGY_SPACE_SHARED = "shared"
# Native Synology thumbnail sizes. ``xl`` is the largest (best for a slideshow).
SYNOLOGY_IMAGE_SMALL = "sm"
SYNOLOGY_IMAGE_MEDIUM = "m"
SYNOLOGY_IMAGE_LARGE = "xl"
SYNOLOGY_IMAGE_SIZE_OPTIONS = [
SYNOLOGY_IMAGE_SMALL,
SYNOLOGY_IMAGE_MEDIUM,
SYNOLOGY_IMAGE_LARGE,
]
DEFAULT_SYNOLOGY_IMAGE_SIZE = SYNOLOGY_IMAGE_LARGE
# iCloud Shared Album provider. The share token in the pasted link is the only
# credential; no account or password is involved.
@@ -44,6 +44,15 @@ from .const import (
CONF_ICLOUD_TOKEN,
CONF_ICLOUD_IMAGE_SIZE,
DEFAULT_ICLOUD_IMAGE_SIZE,
CONF_SYNOLOGY_URL,
CONF_SYNOLOGY_USERNAME,
CONF_SYNOLOGY_PASSWORD,
CONF_SYNOLOGY_DEVICE_ID,
CONF_SYNOLOGY_SPACE,
CONF_SYNOLOGY_ALBUM_ID,
CONF_SYNOLOGY_IMAGE_SIZE,
DEFAULT_SYNOLOGY_IMAGE_SIZE,
SYNOLOGY_SPACE_PERSONAL,
DEFAULT_REVERSE_GEOCODE,
DOMAIN,
PROVIDER_GOOGLE_SHARED,
@@ -52,6 +61,7 @@ from .const import (
PROVIDER_IMMICH,
PROVIDER_PHOTOPRISM,
PROVIDER_ICLOUD,
PROVIDER_SYNOLOGY,
)
from .store import SlideshowStore
@@ -939,6 +949,8 @@ class AlbumCoordinator(DataUpdateCoordinator):
data = await self._update_photoprism()
elif self.provider == PROVIDER_ICLOUD:
data = await self._update_icloud()
elif self.provider == PROVIDER_SYNOLOGY:
data = await self._update_synology()
else:
raise UpdateFailed(f"Unsupported provider: {self.provider}")
except UpdateFailed:
@@ -1512,6 +1524,87 @@ class AlbumCoordinator(DataUpdateCoordinator):
"items": items,
}
async def _update_synology(self) -> dict[str, Any]:
"""Fetch photos from a Synology Photos library via its web API.
Metadata (capture date, GPS, address, description) is returned inline
with each item, so every ``MediaItem`` is built fully here - there is
no background enrichment pass. Thumbnail URLs carry no SID; the session
cookie is stored on the coordinator and sent server-side by the camera
(like the Immich x-api-key), so the SID never reaches the browser.
"""
from . import synology as syn_api
url = self.entry.data.get(CONF_SYNOLOGY_URL)
username = self.entry.data.get(CONF_SYNOLOGY_USERNAME)
password = self.entry.data.get(CONF_SYNOLOGY_PASSWORD)
device_id = self.entry.data.get(CONF_SYNOLOGY_DEVICE_ID)
space = self.entry.data.get(CONF_SYNOLOGY_SPACE, SYNOLOGY_SPACE_PERSONAL)
album_id = self.entry.data.get(CONF_SYNOLOGY_ALBUM_ID)
size = self.entry.data.get(
CONF_SYNOLOGY_IMAGE_SIZE, DEFAULT_SYNOLOGY_IMAGE_SIZE
)
if not url or not username or not password:
raise UpdateFailed("Synology provider is missing URL or credentials")
client = syn_api.SynologyClient(
self.hass,
url,
username=username,
password=password,
device_id=device_id,
space=space,
)
try:
await client.async_login()
photos = await client.async_collect_assets(album_id or None)
except Exception as err:
raise UpdateFailed(f"Error querying Synology Photos: {err}") from err
if not photos:
await client.async_logout()
raise UpdateFailed("No images found for the selected Synology source")
# Store the session cookie so the camera can fetch thumbnail bytes
# server-side. Do not log out: the SID must stay valid until the next
# refresh re-authenticates.
self.image_request_headers = dict(client.image_headers)
items: list[MediaItem] = []
for p in photos:
ref = syn_api.thumbnail_ref(p)
if not ref:
continue
unit_id, cache_key = ref
meta = syn_api.parse_photo_meta(p)
items.append(
MediaItem(
url=syn_api.build_thumbnail_url(
client.base_url, unit_id, cache_key, size, space
),
width=meta.get("width"),
height=meta.get("height"),
mime_type=None,
filename=p.get("filename"),
captured_at=meta.get("captured_at"),
byte_size=meta.get("byte_size"),
latitude=meta.get("latitude"),
longitude=meta.get("longitude"),
location=meta.get("location"),
description=meta.get("description"),
source_id=str(p.get("id")) if p.get("id") is not None else None,
exif_scanned=True,
)
)
if not items:
raise UpdateFailed("Could not resolve any Synology images")
return {
"title": self.entry.title,
"items": items,
}
async def _enrich_immich_item(self, item: MediaItem) -> None:
"""Fetch one Immich asset's detail and fill location/description."""
from . import immich as immich_api
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/eyalgal/album_slideshow/issues",
"requirements": ["Pillow"],
"version": "1.4.0"
"version": "1.5.0"
}
+23 -1
View File
@@ -84,6 +84,26 @@
"icloud_url": "Shared album link",
"icloud_image_size": "Image quality"
}
},
"synology": {
"title": "Synology Photos",
"description": "Connect to the Photos package on your Synology NAS. Enter the DSM address (e.g. http://192.168.1.10:5000 or your HTTPS/QuickConnect URL) and an account. If the account has two-factor authentication, also enter a current 6-digit code once; a trusted-device token is then stored so future refreshes do not need a code. Tip: use a dedicated read-only Photos account rather than an admin login.",
"data": {
"synology_url": "DSM URL",
"synology_username": "Username",
"synology_password": "Password",
"synology_space": "Library",
"otp_code": "2FA code (only if enabled)"
}
},
"synology_select": {
"title": "Synology source",
"description": "Pick an album or show the whole library, and choose the image quality.",
"data": {
"album_name": "Album name",
"album": "Album",
"synology_image_size": "Image quality"
}
}
},
"error": {
@@ -100,7 +120,9 @@
"photoprism_token_required": "Enter an app password, or switch to Username + password.",
"photoprism_user_required": "Enter both a username and password, or switch to App password.",
"invalid_icloud_url": "That does not look like an iCloud Shared Album link.",
"icloud_cannot_connect": "Could not reach that album. Check the link is a current public share."
"icloud_cannot_connect": "Could not reach that album. Check the link is a current public share.",
"synology_cannot_connect": "Could not connect to Synology. Check the URL, username and password.",
"synology_otp_required": "This account needs a two-factor code. Enter a current 6-digit code to continue."
}
},
"options": {
@@ -84,6 +84,26 @@
"icloud_url": "Shared album link",
"icloud_image_size": "Image quality"
}
},
"synology": {
"title": "Synology Photos",
"description": "Connect to the Photos package on your Synology NAS. Enter the DSM address (e.g. http://192.168.1.10:5000 or your HTTPS/QuickConnect URL) and an account. If the account has two-factor authentication, also enter a current 6-digit code once; a trusted-device token is then stored so future refreshes do not need a code. Tip: use a dedicated read-only Photos account rather than an admin login.",
"data": {
"synology_url": "DSM URL",
"synology_username": "Username",
"synology_password": "Password",
"synology_space": "Library",
"otp_code": "2FA code (only if enabled)"
}
},
"synology_select": {
"title": "Synology source",
"description": "Pick an album or show the whole library, and choose the image quality.",
"data": {
"album_name": "Album name",
"album": "Album",
"synology_image_size": "Image quality"
}
}
},
"error": {
@@ -100,7 +120,9 @@
"photoprism_token_required": "Enter an app password, or switch to Username + password.",
"photoprism_user_required": "Enter both a username and password, or switch to App password.",
"invalid_icloud_url": "That does not look like an iCloud Shared Album link.",
"icloud_cannot_connect": "Could not reach that album. Check the link is a current public share."
"icloud_cannot_connect": "Could not reach that album. Check the link is a current public share.",
"synology_cannot_connect": "Could not connect to Synology. Check the URL, username and password.",
"synology_otp_required": "This account needs a two-factor code. Enter a current 6-digit code to continue."
}
},
"options": {
@@ -26,7 +26,7 @@
* tap_action: none # none | more-info
*/
const VERSION = "1.4.0";
const VERSION = "1.5.0";
const ANIMATED_TRANSITIONS = [
"fade",
+2 -1
View File
@@ -820,7 +820,8 @@ class AlexaMediaFlowHandler(config_entries.ConfigFlow):
if CONF_PASSWORD in user_input:
self.config[CONF_PASSWORD] = user_input[CONF_PASSWORD]
if CONF_URL in user_input:
self.config[CONF_URL] = user_input[CONF_URL]
# Remove accidental whitespace introduced by mobile keyboards/paste.
self.config[CONF_URL] = "".join(user_input[CONF_URL].split())
if CONF_PUBLIC_URL in user_input:
if not user_input[CONF_PUBLIC_URL].endswith("/"):
user_input[CONF_PUBLIC_URL] = user_input[CONF_PUBLIC_URL] + "/"
+1 -1
View File
@@ -14,5 +14,5 @@
"wrapt>=1.14.0",
"dictor>=0.1.12,<0.2"
],
"version": "5.15.6"
"version": "5.15.7"
}
+1 -1
View File
@@ -664,7 +664,7 @@ class AlexaMediaNotificationSensor(SensorEntity):
next_item["snoozedToTime"] = None
return value
def _is_active_notification(self, item, now):
def _is_active_notification(self, item, _now):
"""Return whether a notification should be considered active."""
status = item[1].get("status")
if status == "ON":
@@ -3,32 +3,32 @@
"abort": {
"forgot_password": "La page de réinitialisation du mot de passe a été détectée. Cela résulte généralement de trop nombreuses tentatives de connexion échouées. Amazon peut exiger une action avant qu'une nouvelle connexion ne puisse être tentée.",
"login_failed": "Alexa Media Player n'a pas réussi à se connecter.",
"reauth_successful": "Alexa Media Player s'est ré-authentifié avec succès. Veuillez ignorer le message \"Abandonné\" de Home Assistant."
"reauth_successful": "Alexa Media Player s'est ré-authentifié avec succès. Veuillez ignorer le message « Abandonné » de Home Assistant."
},
"error": {
"2fa_key_invalid": "{otp_secret} n'est pas valide",
"connection_error": "Erreur de connexion ; vérifiez le réseau et réessayez",
"connection_error": "Erreur de connexion ; vérifiez le réseau et réessayez",
"identifier_exists": "L'adresse e-mail pour cette URL Alexa est déjà enregistrée",
"invalid_auth": "La connexion a échoué. Veuillez vérifier votre adresse e-mail, votre mot de passe et votre clé d'authentification.",
"invalid_credentials": "Identifiants invalides",
"invalid_url": "L'URL n'est pas valide: {message}",
"invalid_url": "L'URL n'est pas valide : {message}",
"oauth_error": "Impossible de terminer la connexion OAuth. Veuillez réessayer.",
"unable_to_connect_hass_url": "Impossible de se connecter à l'URL locale de Home Assistant. Veuillez vérifier l'URL sous Paramètres > Système > Réseau > URL de Home Assistant > Réseau local",
"unknown_error": "Erreur inconnue : {message}"
"unknown_error": "Erreur inconnue : {message}"
},
"step": {
"proxy_warning": {
"data": {
"proxy_warning": "Ignorer et continuer - Je comprends qu'aucune assistance pour les problèmes de connexion ne sera fournie si je contourne cet avertissement."
},
"description": "Le serveur Home Assistant ne peut pas se connecter à l'URL fournie : {hass_url}.\n> {error}\n\nPour résoudre ce problème, veuillez confirmer que votre navigateur peut atteindre {hass_url}. Ce champ provient de Paramètres > Système > Réseau > URL de Home Assistant.\n\nSi vous êtes **certain** que votre navigateur peut accéder à cette URL, vous pouvez ignorer cet avertissement.",
"description": "Le serveur Home Assistant ne peut pas se connecter à l'URL fournie : {hass_url}.\n> {error}\n\nPour résoudre ce problème, veuillez confirmer que votre navigateur peut atteindre {hass_url}. Ce champ provient de Paramètres > Système > Réseau > URL de Home Assistant.\n\nSi vous êtes **certain** que votre navigateur peut accéder à cette URL, vous pouvez ignorer cet avertissement.",
"title": "Alexa Media Player - Impossible de se connecter à l'URL de HA"
},
"totp_register": {
"data": {
"registered": "Oui, le code OTP a été vérifié"
},
"description": "**{email} - alexa.{url}**\nAvez-vous vérifié le code OTP dans la validation en deux étapes Amazon ?\n> Code OTP : {message}",
"description": "**{email} - alexa.{url}**\nAvez-vous vérifié le code OTP dans la validation en deux étapes Amazon ?\n> Code OTP : {message}",
"title": "Alexa Media Player - Confirmation OTP"
},
"user": {
@@ -46,10 +46,10 @@
"scan_interval": "Intervalle d'interrogation programmé (secondes)",
"securitycode": "Mot de passe à usage unique (OTP)",
"should_get_network": "Découvrir le réseau Alexa",
"url": "Domaine de la région Amazon (ex : amazon.fr)"
"url": "Domaine de la région Amazon (ex : amazon.fr)"
},
"data_description": {
"debug": "Active une journalisation très détaillée, au niveau de la trace, pour un dépannage avancé. \n Non recommandé en fonctionnement normal en raison de l'augmentation du volume des journaux. \n Assurez-vous que le niveau de journalisation est défini sur DEBUG pour obtenir une sortie complète.",
"debug": "Active une journalisation très détaillée, au niveau de la trace, pour un dépannage avancé.\nNon recommandé en fonctionnement normal en raison de l'augmentation du volume des journaux.\nAssurez-vous que le niveau de journalisation est défini sur DEBUG pour obtenir une sortie complète.",
"otp_secret": "Exemple : 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
}
}
@@ -58,7 +58,7 @@
"entity": {
"sensor": {
"air_quality": {
"name": "qualité de l'air"
"name": "Qualité de l'air"
},
"air_quality_carbon_monoxide": {
"name": "Monoxyde de carbone"
@@ -67,7 +67,7 @@
"name": "Humidité"
},
"air_quality_indoor_air_quality": {
"name": "qualité de l'air intérieur"
"name": "Qualité de l'air intérieur"
},
"air_quality_particulate_matter": {
"name": "Matières particulaires"
@@ -82,7 +82,7 @@
"name": "Prochain rappel"
},
"next_timer": {
"name": "La prochaine fois"
"name": "Prochain minuteur"
},
"temperature": {
"name": "Température"
@@ -117,11 +117,11 @@
"otp_secret": "Clé d'authentification à 52 caractères pour Amazon 2SV",
"public_url": "URL publique partagée avec les services externes hébergés",
"queue_delay": "Délai pour regrouper plusieurs commandes (secondes)",
"scan_interval": "Fréquence d'interrogation programmée (secondes)",
"scan_interval": "Intervalle d'interrogation programmé (secondes)",
"should_get_network": "Découvrir le réseau Alexa"
},
"data_description": {
"debug": "Active une journalisation très détaillée, au niveau de la trace, pour un dépannage avancé. \n Non recommandé en fonctionnement normal en raison de l'augmentation du volume des journaux. \n Assurez-vous que le niveau de journalisation est défini sur DEBUG pour obtenir une sortie complète.",
"debug": "Active une journalisation très détaillée, au niveau de la trace, pour un dépannage avancé.\nNon recommandé en fonctionnement normal en raison de l'augmentation du volume des journaux.\nAssurez-vous que le niveau de journalisation est défini sur DEBUG pour obtenir une sortie complète.",
"otp_secret": "Exemple : 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
},
"description": "* Champs obligatoires",
@@ -135,7 +135,7 @@
"fields": {
"email": {
"description": "Adresse e-mail du compte Alexa ou liste d'adresses (facultatif). Si vide, tous les comptes connus seront actualisés.",
"name": "Adresse email"
"name": "Adresse e-mail"
}
},
"name": "Activer la découverte du réseau"
@@ -155,7 +155,7 @@
"fields": {
"entity_id": {
"description": "Entité pour laquelle obtenir l'historique",
"name": "Sélectionner le lecteur multimédia:"
"name": "Sélectionner le lecteur multimédia :"
},
"entries": {
"description": "Nombre d'entrées à récupérer",
@@ -169,7 +169,7 @@
"fields": {
"entity_id": {
"description": "Entité sur laquelle restaurer le niveau de volume précédent",
"name": "Sélectionner le lecteur multimédia:"
"name": "Sélectionner le lecteur multimédia :"
}
},
"name": "Restaurer le volume précédent"
+2 -1
View File
@@ -1000,7 +1000,8 @@ def _is_path_allowed_for_read(
Allowed:
- Files directly in config dir: configuration.yaml, automations.yaml, etc.
- Files in allowed directories: www/, themes/, custom_templates/
- Files in allowed directories: www/, themes/, custom_templates/,
dashboards/, blueprints/ (blueprints is read-only issue #1965)
- Files matching patterns: <packages-folder>/*.yaml, custom_components/**/*.py
- User-configured extra directories (``extra_dirs``), granted read+write
(issue #1567)

Some files were not shown because too many files have changed in this diff Show More