348 files
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
2026.7.2
|
2026.7.3
|
||||||
+25
-13
@@ -1,14 +1,26 @@
|
|||||||
# Home Assistant Core Exclusions
|
# Only track specific file types
|
||||||
*.db
|
# Ignore everything by default
|
||||||
*.db-shm
|
*
|
||||||
*.db-wal
|
|
||||||
*.log
|
# === Home Assistant Version Control Managed (do not edit below) ===
|
||||||
.storage/
|
# 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
|
secrets.yaml
|
||||||
*icon*.png
|
|
||||||
*state.json
|
|
||||||
|
|
||||||
|
|
||||||
# Custom integrations and private tokens
|
|
||||||
.google_nest_prompt_info.json
|
|
||||||
nest_protect_cookies.json
|
|
||||||
|
|||||||
+1
-1
@@ -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}
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"minor_version": 1,
|
||||||
|
"key": "lovelace.map",
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"strategy": {
|
||||||
|
"type": "map"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,44 @@
|
|||||||
|
|
||||||
You are working directly within a Home Assistant installation. Your working directory is `/homeassistant`, which is the live Home Assistant configuration directory.
|
You are working directly within a Home Assistant installation. Your working directory is `/homeassistant`, which is the live Home Assistant configuration directory.
|
||||||
|
|
||||||
|
## Repo-Specific Facts
|
||||||
|
|
||||||
|
### Version and Structure
|
||||||
|
- **HA Version**: 2026.7.3 (see `.HA_VERSION`)
|
||||||
|
- **Config entry point**: `configuration.yaml` — flat config, no `!include_dir` for automations (all in `automations.yaml`)
|
||||||
|
- **No packages directory** — all automations live in a single `automations.yaml` (~2960 lines)
|
||||||
|
- **Prettier** is configured (`.prettierrc.yaml`) and auto-formats YAML on save; it enforces 2-space indent and double quotes but NOT block-style enforcement or boolean normalization
|
||||||
|
|
||||||
|
### Blink Camera Dual-Entity Setup (CRITICAL)
|
||||||
|
Blink cameras are connected via **two Homebridge plugins** plus the native HA Blink integration. This creates **two sets of camera entities per physical camera**:
|
||||||
|
|
||||||
|
| Physical Camera | HomeKit (Homebridge) | BlinkCameras (Homebridge) | Native HA Blink |
|
||||||
|
|----------------|---------------------|--------------------------|-----------------|
|
||||||
|
| Front | `camera.front` | `camera.blink_front` | — |
|
||||||
|
| Backyard | `camera.backyard` | `camera.blink_backyard` | — |
|
||||||
|
| Shed | `camera.backyard_shed` | `camera.blink_backyard_shed` | — |
|
||||||
|
| Tree | `camera.backyard_tree` | `camera.blink_backyard_tree` | — |
|
||||||
|
| Garage Side | — | `camera.blink_garage_side` | — |
|
||||||
|
| Thermopump | — | `camera.blink_thermopump` | — |
|
||||||
|
|
||||||
|
**Refresh switches** (`switch.*_refresh_snapshot`) come from the BlinkCameras plugin. When writing automations that take snapshots, **match the camera entity to the plugin that owns the refresh switch** — using a HomeKit camera with a BlinkCameras refresh switch produces stale images. Use `camera.blink_*` entities for snapshot automation.
|
||||||
|
|
||||||
|
The `camera.front` → `switch.front_refresh_snapshot` pairing is the known working combination.
|
||||||
|
|
||||||
|
### Critical Device Rules
|
||||||
|
- **`switch.dragon_egg`** (living room smart plug) — **NEVER turn off after 10 PM**. Always check time before any service call targeting this entity.
|
||||||
|
|
||||||
|
### Custom Integrations (33 installed)
|
||||||
|
Key ones: `hacs`, `alexa_media`, `hilo`, `powercalc`, `versatile_thermostat`, `webrtc`, `nest_protect`, `midea_ac_lan`, `spook`, `watchman`, `alarmo`, `cync_lights`, `wyzeapi`, `asusrouter`, `trueNas`.
|
||||||
|
|
||||||
|
### Zigbee2MQTT
|
||||||
|
Config at `zigbee2mqtt/configuration.yaml`. Use `zigporter` for device management (cascade renames, mesh visualization). `hab` can list devices but does NOT cascade references.
|
||||||
|
|
||||||
|
### Reload vs Restart
|
||||||
|
- After editing `automations.yaml`: call `automation.reload` service (no restart needed)
|
||||||
|
- After editing `configuration.yaml`: requires HA restart
|
||||||
|
- After editing `scripts.yaml`: call `script.reload` service
|
||||||
|
|
||||||
## CRITICAL: User Consent and Scope Rules
|
## CRITICAL: User Consent and Scope Rules
|
||||||
|
|
||||||
You MUST follow these rules strictly:
|
You MUST follow these rules strictly:
|
||||||
|
|||||||
+395
-394
File diff suppressed because it is too large
Load Diff
@@ -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
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -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)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -54,6 +54,19 @@ from .const import (
|
|||||||
DEFAULT_ICLOUD_IMAGE_SIZE,
|
DEFAULT_ICLOUD_IMAGE_SIZE,
|
||||||
ICLOUD_IMAGE_FULL,
|
ICLOUD_IMAGE_FULL,
|
||||||
ICLOUD_IMAGE_PREVIEW,
|
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,
|
DEFAULT_REVERSE_GEOCODE,
|
||||||
PROVIDER_GOOGLE_SHARED,
|
PROVIDER_GOOGLE_SHARED,
|
||||||
PROVIDER_LOCAL_FOLDER,
|
PROVIDER_LOCAL_FOLDER,
|
||||||
@@ -61,6 +74,7 @@ from .const import (
|
|||||||
PROVIDER_IMMICH,
|
PROVIDER_IMMICH,
|
||||||
PROVIDER_PHOTOPRISM,
|
PROVIDER_PHOTOPRISM,
|
||||||
PROVIDER_ICLOUD,
|
PROVIDER_ICLOUD,
|
||||||
|
PROVIDER_SYNOLOGY,
|
||||||
DEFAULT_RECURSIVE,
|
DEFAULT_RECURSIVE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,6 +122,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self._pp_password: str | None = None
|
self._pp_password: str | None = None
|
||||||
self._pp_albums: dict[str, str] = {}
|
self._pp_albums: dict[str, str] = {}
|
||||||
self._pp_people: 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
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
@@ -143,6 +164,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
return await self.async_step_photoprism()
|
return await self.async_step_photoprism()
|
||||||
if self._provider == PROVIDER_ICLOUD:
|
if self._provider == PROVIDER_ICLOUD:
|
||||||
return await self.async_step_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()
|
return await self.async_step_google_shared()
|
||||||
|
|
||||||
schema = vol.Schema(
|
schema = vol.Schema(
|
||||||
@@ -153,6 +176,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
PROVIDER_IMMICH: "Immich (direct API, full metadata)",
|
PROVIDER_IMMICH: "Immich (direct API, full metadata)",
|
||||||
PROVIDER_PHOTOPRISM: "PhotoPrism (direct API, full metadata)",
|
PROVIDER_PHOTOPRISM: "PhotoPrism (direct API, full metadata)",
|
||||||
PROVIDER_ICLOUD: "iCloud Shared Album",
|
PROVIDER_ICLOUD: "iCloud Shared Album",
|
||||||
|
PROVIDER_SYNOLOGY: "Synology Photos (direct API, full metadata)",
|
||||||
PROVIDER_MEDIA_SOURCE: "Media Source (any source, no 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
|
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):
|
class LocalFolderOptionsFlow(config_entries.OptionsFlow):
|
||||||
"""Options for local-folder entries.
|
"""Options for local-folder entries.
|
||||||
|
|||||||
@@ -57,6 +57,34 @@ PROVIDER_MEDIA_SOURCE = "media_source"
|
|||||||
PROVIDER_IMMICH = "immich"
|
PROVIDER_IMMICH = "immich"
|
||||||
PROVIDER_PHOTOPRISM = "photoprism"
|
PROVIDER_PHOTOPRISM = "photoprism"
|
||||||
PROVIDER_ICLOUD = "icloud"
|
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
|
# iCloud Shared Album provider. The share token in the pasted link is the only
|
||||||
# credential; no account or password is involved.
|
# credential; no account or password is involved.
|
||||||
|
|||||||
@@ -44,6 +44,15 @@ from .const import (
|
|||||||
CONF_ICLOUD_TOKEN,
|
CONF_ICLOUD_TOKEN,
|
||||||
CONF_ICLOUD_IMAGE_SIZE,
|
CONF_ICLOUD_IMAGE_SIZE,
|
||||||
DEFAULT_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,
|
DEFAULT_REVERSE_GEOCODE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PROVIDER_GOOGLE_SHARED,
|
PROVIDER_GOOGLE_SHARED,
|
||||||
@@ -52,6 +61,7 @@ from .const import (
|
|||||||
PROVIDER_IMMICH,
|
PROVIDER_IMMICH,
|
||||||
PROVIDER_PHOTOPRISM,
|
PROVIDER_PHOTOPRISM,
|
||||||
PROVIDER_ICLOUD,
|
PROVIDER_ICLOUD,
|
||||||
|
PROVIDER_SYNOLOGY,
|
||||||
)
|
)
|
||||||
from .store import SlideshowStore
|
from .store import SlideshowStore
|
||||||
|
|
||||||
@@ -939,6 +949,8 @@ class AlbumCoordinator(DataUpdateCoordinator):
|
|||||||
data = await self._update_photoprism()
|
data = await self._update_photoprism()
|
||||||
elif self.provider == PROVIDER_ICLOUD:
|
elif self.provider == PROVIDER_ICLOUD:
|
||||||
data = await self._update_icloud()
|
data = await self._update_icloud()
|
||||||
|
elif self.provider == PROVIDER_SYNOLOGY:
|
||||||
|
data = await self._update_synology()
|
||||||
else:
|
else:
|
||||||
raise UpdateFailed(f"Unsupported provider: {self.provider}")
|
raise UpdateFailed(f"Unsupported provider: {self.provider}")
|
||||||
except UpdateFailed:
|
except UpdateFailed:
|
||||||
@@ -1512,6 +1524,87 @@ class AlbumCoordinator(DataUpdateCoordinator):
|
|||||||
"items": items,
|
"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:
|
async def _enrich_immich_item(self, item: MediaItem) -> None:
|
||||||
"""Fetch one Immich asset's detail and fill location/description."""
|
"""Fetch one Immich asset's detail and fill location/description."""
|
||||||
from . import immich as immich_api
|
from . import immich as immich_api
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"issue_tracker": "https://github.com/eyalgal/album_slideshow/issues",
|
"issue_tracker": "https://github.com/eyalgal/album_slideshow/issues",
|
||||||
"requirements": ["Pillow"],
|
"requirements": ["Pillow"],
|
||||||
"version": "1.4.0"
|
"version": "1.5.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,26 @@
|
|||||||
"icloud_url": "Shared album link",
|
"icloud_url": "Shared album link",
|
||||||
"icloud_image_size": "Image quality"
|
"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": {
|
"error": {
|
||||||
@@ -100,7 +120,9 @@
|
|||||||
"photoprism_token_required": "Enter an app password, or switch to Username + password.",
|
"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.",
|
"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.",
|
"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": {
|
"options": {
|
||||||
|
|||||||
@@ -84,6 +84,26 @@
|
|||||||
"icloud_url": "Shared album link",
|
"icloud_url": "Shared album link",
|
||||||
"icloud_image_size": "Image quality"
|
"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": {
|
"error": {
|
||||||
@@ -100,7 +120,9 @@
|
|||||||
"photoprism_token_required": "Enter an app password, or switch to Username + password.",
|
"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.",
|
"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.",
|
"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": {
|
"options": {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
* tap_action: none # none | more-info
|
* tap_action: none # none | more-info
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const VERSION = "1.4.0";
|
const VERSION = "1.5.0";
|
||||||
|
|
||||||
const ANIMATED_TRANSITIONS = [
|
const ANIMATED_TRANSITIONS = [
|
||||||
"fade",
|
"fade",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -820,7 +820,8 @@ class AlexaMediaFlowHandler(config_entries.ConfigFlow):
|
|||||||
if CONF_PASSWORD in user_input:
|
if CONF_PASSWORD in user_input:
|
||||||
self.config[CONF_PASSWORD] = user_input[CONF_PASSWORD]
|
self.config[CONF_PASSWORD] = user_input[CONF_PASSWORD]
|
||||||
if CONF_URL in user_input:
|
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 CONF_PUBLIC_URL in user_input:
|
||||||
if not user_input[CONF_PUBLIC_URL].endswith("/"):
|
if not user_input[CONF_PUBLIC_URL].endswith("/"):
|
||||||
user_input[CONF_PUBLIC_URL] = user_input[CONF_PUBLIC_URL] + "/"
|
user_input[CONF_PUBLIC_URL] = user_input[CONF_PUBLIC_URL] + "/"
|
||||||
|
|||||||
@@ -14,5 +14,5 @@
|
|||||||
"wrapt>=1.14.0",
|
"wrapt>=1.14.0",
|
||||||
"dictor>=0.1.12,<0.2"
|
"dictor>=0.1.12,<0.2"
|
||||||
],
|
],
|
||||||
"version": "5.15.6"
|
"version": "5.15.7"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -664,7 +664,7 @@ class AlexaMediaNotificationSensor(SensorEntity):
|
|||||||
next_item["snoozedToTime"] = None
|
next_item["snoozedToTime"] = None
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def _is_active_notification(self, item, now):
|
def _is_active_notification(self, item, _now):
|
||||||
"""Return whether a notification should be considered active."""
|
"""Return whether a notification should be considered active."""
|
||||||
status = item[1].get("status")
|
status = item[1].get("status")
|
||||||
if status == "ON":
|
if status == "ON":
|
||||||
|
|||||||
@@ -3,32 +3,32 @@
|
|||||||
"abort": {
|
"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.",
|
"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.",
|
"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": {
|
"error": {
|
||||||
"2fa_key_invalid": "{otp_secret} n'est pas valide",
|
"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",
|
"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_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_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.",
|
"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",
|
"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": {
|
"step": {
|
||||||
"proxy_warning": {
|
"proxy_warning": {
|
||||||
"data": {
|
"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."
|
"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"
|
"title": "Alexa Media Player - Impossible de se connecter à l'URL de HA"
|
||||||
},
|
},
|
||||||
"totp_register": {
|
"totp_register": {
|
||||||
"data": {
|
"data": {
|
||||||
"registered": "Oui, le code OTP a été vérifié"
|
"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"
|
"title": "Alexa Media Player - Confirmation OTP"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
"scan_interval": "Intervalle d'interrogation programmé (secondes)",
|
"scan_interval": "Intervalle d'interrogation programmé (secondes)",
|
||||||
"securitycode": "Mot de passe à usage unique (OTP)",
|
"securitycode": "Mot de passe à usage unique (OTP)",
|
||||||
"should_get_network": "Découvrir le réseau Alexa",
|
"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": {
|
"data_description": {
|
||||||
"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.",
|
"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.",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"air_quality": {
|
"air_quality": {
|
||||||
"name": "qualité de l'air"
|
"name": "Qualité de l'air"
|
||||||
},
|
},
|
||||||
"air_quality_carbon_monoxide": {
|
"air_quality_carbon_monoxide": {
|
||||||
"name": "Monoxyde de carbone"
|
"name": "Monoxyde de carbone"
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
"name": "Humidité"
|
"name": "Humidité"
|
||||||
},
|
},
|
||||||
"air_quality_indoor_air_quality": {
|
"air_quality_indoor_air_quality": {
|
||||||
"name": "qualité de l'air intérieur"
|
"name": "Qualité de l'air intérieur"
|
||||||
},
|
},
|
||||||
"air_quality_particulate_matter": {
|
"air_quality_particulate_matter": {
|
||||||
"name": "Matières particulaires"
|
"name": "Matières particulaires"
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
"name": "Prochain rappel"
|
"name": "Prochain rappel"
|
||||||
},
|
},
|
||||||
"next_timer": {
|
"next_timer": {
|
||||||
"name": "La prochaine fois"
|
"name": "Prochain minuteur"
|
||||||
},
|
},
|
||||||
"temperature": {
|
"temperature": {
|
||||||
"name": "Température"
|
"name": "Température"
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
"otp_secret": "Clé d'authentification à 52 caractères pour Amazon 2SV",
|
"otp_secret": "Clé d'authentification à 52 caractères pour Amazon 2SV",
|
||||||
"public_url": "URL publique partagée avec les services externes hébergés",
|
"public_url": "URL publique partagée avec les services externes hébergés",
|
||||||
"queue_delay": "Délai pour regrouper plusieurs commandes (secondes)",
|
"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"
|
"should_get_network": "Découvrir le réseau Alexa"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"email": {
|
"email": {
|
||||||
"description": "Adresse e-mail du compte Alexa ou liste d'adresses (facultatif). Si vide, tous les comptes connus seront actualisés.",
|
"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"
|
"name": "Activer la découverte du réseau"
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"entity_id": {
|
"entity_id": {
|
||||||
"description": "Entité pour laquelle obtenir l'historique",
|
"description": "Entité pour laquelle obtenir l'historique",
|
||||||
"name": "Sélectionner le lecteur multimédia:"
|
"name": "Sélectionner le lecteur multimédia :"
|
||||||
},
|
},
|
||||||
"entries": {
|
"entries": {
|
||||||
"description": "Nombre d'entrées à récupérer",
|
"description": "Nombre d'entrées à récupérer",
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"entity_id": {
|
"entity_id": {
|
||||||
"description": "Entité sur laquelle restaurer le niveau de volume précédent",
|
"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"
|
"name": "Restaurer le volume précédent"
|
||||||
|
|||||||
@@ -1000,7 +1000,8 @@ def _is_path_allowed_for_read(
|
|||||||
|
|
||||||
Allowed:
|
Allowed:
|
||||||
- Files directly in config dir: configuration.yaml, automations.yaml, etc.
|
- 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
|
- Files matching patterns: <packages-folder>/*.yaml, custom_components/**/*.py
|
||||||
- User-configured extra directories (``extra_dirs``), granted read+write
|
- User-configured extra directories (``extra_dirs``), granted read+write
|
||||||
(issue #1567)
|
(issue #1567)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -24,7 +24,7 @@ DOMAIN = "ha_mcp_tools"
|
|||||||
# manifest bump that forgets this constant (or vice-versa) fails in CI. The
|
# manifest bump that forgets this constant (or vice-versa) fails in CI. The
|
||||||
# capability negotiation — not this version — gates each WS command (see
|
# capability negotiation — not this version — gates each WS command (see
|
||||||
# ``websocket_api.CAPABILITIES``).
|
# ``websocket_api.CAPABILITIES``).
|
||||||
COMPONENT_VERSION = "1.2.2"
|
COMPONENT_VERSION = "1.2.3"
|
||||||
|
|
||||||
# Config-entry discriminator (``entry.data[CONF_ENTRY_TYPE]``). A missing value
|
# Config-entry discriminator (``entry.data[CONF_ENTRY_TYPE]``). A missing value
|
||||||
# means "tools" so the pre-existing services entry keeps working across the
|
# means "tools" so the pre-existing services entry keeps working across the
|
||||||
@@ -40,8 +40,17 @@ TOOLS_ENTRY_TITLE = "HA-MCP File & YAML Tools"
|
|||||||
TOOLS_ENTRY_LEGACY_TITLE = "HA MCP Tools"
|
TOOLS_ENTRY_LEGACY_TITLE = "HA MCP Tools"
|
||||||
MIN_EMBEDDED_HOME_ASSISTANT_VERSION = "2026.6.0"
|
MIN_EMBEDDED_HOME_ASSISTANT_VERSION = "2026.6.0"
|
||||||
|
|
||||||
# Allowed directories for file operations (relative to config dir)
|
# Allowed directories for file operations (relative to config dir).
|
||||||
ALLOWED_READ_DIRS = ["www", "themes", "custom_templates", "dashboards"]
|
# "blueprints" is read-only BY DEFAULT — in ALLOWED_READ_DIRS but not
|
||||||
|
# ALLOWED_WRITE_DIRS, so ha_write_file / ha_delete_file reject it (raw blueprint
|
||||||
|
# reads are safe: community YAML, no secrets — issue #1965). This is the default
|
||||||
|
# allowlist, not an absolute guarantee: an admin who adds "blueprints" as a
|
||||||
|
# custom extra directory (issue #1567, see _current_extra_dirs) grants it
|
||||||
|
# read+write, since extra_dirs are honored on the write path too. Blueprint
|
||||||
|
# writes should instead go through ha_import_blueprint (which invokes the
|
||||||
|
# blueprint/save WS command internally). Prefer ha_get_blueprint for the parsed
|
||||||
|
# body; raw read is the escape hatch for the exact on-disk text.
|
||||||
|
ALLOWED_READ_DIRS = ["www", "themes", "custom_templates", "dashboards", "blueprints"]
|
||||||
ALLOWED_WRITE_DIRS = ["www", "themes", "custom_templates", "dashboards"]
|
ALLOWED_WRITE_DIRS = ["www", "themes", "custom_templates", "dashboards"]
|
||||||
|
|
||||||
# NON-OVERRIDABLE deny floor for the user-configurable extra read/write
|
# NON-OVERRIDABLE deny floor for the user-configurable extra read/write
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user