From efe82f35faffe6afd5c3ffe221f72d1a0b900ac1 Mon Sep 17 00:00:00 2001 From: DaRacci Date: Thu, 8 Jan 2026 15:08:37 +1100 Subject: [PATCH] feat(hosts/server/nixcloud): home assistant refactor and delcarative UI --- .../bedroom_ac_adaptive_fan_speed.yaml | 133 ++ .../nixcloud/home-assistant/dashboard.nix | 2 +- .../dashboard/components/admin-panel.nix | 19 +- .../dashboard/components/button-cards.nix | 1682 +++++++++++++++++ .../components/button-cards/default.nix | 6 + .../components/button-cards/media-player.nix | 351 ++++ .../components/button-cards/media_player.nix | 47 - .../decluttering.nix | 0 .../dashboard/components/navbar.nix | 28 +- .../dashboard/components/templates/popup.nix | 15 + .../home-assistant/dashboard/default.nix | 6 +- .../home-assistant/dashboard/kiosk-mode.nix | 9 +- .../nixcloud/home-assistant/dashboard/lib.nix | 584 +++++- .../dashboard/templates/button-cards.nix | 812 -------- .../home-assistant/dashboard/views/energy.nix | 15 +- .../home-assistant/dashboard/views/home.nix | 413 +++- .../home-assistant/dashboard/views/music.nix | 825 +------- .../dashboard/views/security.nix | 2 +- .../home-assistant/dashboard/views/server.nix | 2 +- .../nixcloud/home-assistant/default.nix | 4 +- .../server/nixcloud/home-assistant/music.nix | 5 +- .../server/nixcloud/home-assistant/static.nix | 24 + 22 files changed, 3315 insertions(+), 1669 deletions(-) create mode 100644 hosts/server/nixcloud/home-assistant/automations/bedroom_ac_adaptive_fan_speed.yaml create mode 100644 hosts/server/nixcloud/home-assistant/dashboard/components/button-cards.nix create mode 100644 hosts/server/nixcloud/home-assistant/dashboard/components/button-cards/default.nix create mode 100644 hosts/server/nixcloud/home-assistant/dashboard/components/button-cards/media-player.nix delete mode 100644 hosts/server/nixcloud/home-assistant/dashboard/components/button-cards/media_player.nix rename hosts/server/nixcloud/home-assistant/dashboard/{templates => components}/decluttering.nix (100%) create mode 100644 hosts/server/nixcloud/home-assistant/dashboard/components/templates/popup.nix delete mode 100644 hosts/server/nixcloud/home-assistant/dashboard/templates/button-cards.nix create mode 100644 hosts/server/nixcloud/home-assistant/static.nix diff --git a/hosts/server/nixcloud/home-assistant/automations/bedroom_ac_adaptive_fan_speed.yaml b/hosts/server/nixcloud/home-assistant/automations/bedroom_ac_adaptive_fan_speed.yaml new file mode 100644 index 000000000..246513cd1 --- /dev/null +++ b/hosts/server/nixcloud/home-assistant/automations/bedroom_ac_adaptive_fan_speed.yaml @@ -0,0 +1,133 @@ +- id: bedroom_ac_adaptive_fan_speed + alias: Bedroom AC - Adaptive Fan Speed + description: > + Automatically adjusts bedroom AC fan speed based on how quickly the room's + temperature distance to target is shrinking. + mode: single + + trigger: + - platform: time_pattern + minutes: "/5" + - platform: state + entity_id: sensor.bedroom_ac_temperature + - platform: state + entity_id: climate.bedroom_ac + attribute: current_temperature + + condition: + # Only run while the AC is actively heating or cooling. + - condition: template + value_template: "{{ states('climate.bedroom_ac') in ['cool', 'heat'] }}" + + # Cooldown: if this automation changed fan speed in the last 5 minutes, stop. + - condition: template + value_template: > + {% set ts = as_timestamp(states('input_datetime.bedroom_ac_fan_last_adjusted')) %} + {{ ts is none or (as_timestamp(now()) - ts) >= 300 }} + + action: + - variables: + current_temp: "{{ state_attr('climate.bedroom_ac', 'current_temperature') | float(0) }}" + target_temp: "{{ states('input_number.target_temperature') | float(0) }}" + current_distance: "{{ (current_temp - target_temp) | abs }}" + + elapsed_minutes: > + {% set ts = as_timestamp(states('input_datetime.bedroom_ac_fan_last_adjusted')) %} + {% if ts is none %} + 15 + {% else %} + {{ ((as_timestamp(now()) - ts) / 60) | float(0) }} + {% endif %} + + # Use the shorter window: time since last fan adjustment or 15 minutes. + # Implemented as 5/10/15 minute buckets (automation cadence is 5 minutes). + window_minutes: > + {% set e = elapsed_minutes | float(15) %} + {% if e < 10 %} + 5 + {% elif e < 15 %} + 10 + {% else %} + 15 + {% endif %} + + selected_change_sensor: > + {% if (window_minutes | int) == 5 %} + sensor.bedroom_ac_distance_change_5m + {% elif (window_minutes | int) == 10 %} + sensor.bedroom_ac_distance_change_10m + {% else %} + sensor.bedroom_ac_distance_change_15m + {% endif %} + + # Statistics change sensor is newest - oldest. + # If distance to target is reducing, this value is negative. + distance_change: "{{ states(selected_change_sensor) | float(0) }}" + distance_reduced_by: "{{ (0 - distance_change) | float(0) }}" + + fan_modes: + - quiet + - low + - medium_low + - medium + - medium_high + - high + + current_fan_mode: "{{ state_attr('climate.bedroom_ac', 'fan_mode') | default('medium', true) }}" + current_index: > + {% if current_fan_mode in fan_modes %} + {{ fan_modes.index(current_fan_mode) }} + {% else %} + {{ fan_modes.index('medium') }} + {% endif %} + + increase_fan_mode: > + {% if (current_index | int) < (fan_modes | count - 1) %} + {{ fan_modes[current_index | int + 1] }} + {% else %} + {{ current_fan_mode }} + {% endif %} + + decrease_fan_mode: > + {% if (current_index | int) > 0 %} + {{ fan_modes[current_index | int - 1] }} + {% else %} + {{ current_fan_mode }} + {% endif %} + + - choose: + # If we're reducing distance fast (>0.4) and we're already close (<2.0), reduce fan speed. + - conditions: + - condition: template + value_template: "{{ distance_reduced_by > 0.4 and current_distance < 2 }}" + - condition: template + value_template: "{{ decrease_fan_mode != current_fan_mode }}" + sequence: + - service: climate.set_fan_mode + target: + entity_id: climate.bedroom_ac + data: + fan_mode: "{{ decrease_fan_mode }}" + - service: input_datetime.set_datetime + target: + entity_id: input_datetime.bedroom_ac_fan_last_adjusted + data: + datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}" + + # If distance has not reduced by at least 0.2, increase fan speed. + - conditions: + - condition: template + value_template: "{{ distance_reduced_by < 0.2 }}" + - condition: template + value_template: "{{ increase_fan_mode != current_fan_mode }}" + sequence: + - service: climate.set_fan_mode + target: + entity_id: climate.bedroom_ac + data: + fan_mode: "{{ increase_fan_mode }}" + - service: input_datetime.set_datetime + target: + entity_id: input_datetime.bedroom_ac_fan_last_adjusted + data: + datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}" diff --git a/hosts/server/nixcloud/home-assistant/dashboard.nix b/hosts/server/nixcloud/home-assistant/dashboard.nix index 814c57420..9320584a4 100644 --- a/hosts/server/nixcloud/home-assistant/dashboard.nix +++ b/hosts/server/nixcloud/home-assistant/dashboard.nix @@ -150,7 +150,7 @@ ]; lovelace = { - mode = "yaml"; + resource_mode = "yaml"; resources = [ { url = "/local/nixos-lovelace-modules/bubble-pop-up-fix.js"; diff --git a/hosts/server/nixcloud/home-assistant/dashboard/components/admin-panel.nix b/hosts/server/nixcloud/home-assistant/dashboard/components/admin-panel.nix index a7830035b..a64fd0469 100644 --- a/hosts/server/nixcloud/home-assistant/dashboard/components/admin-panel.nix +++ b/hosts/server/nixcloud/home-assistant/dashboard/components/admin-panel.nix @@ -1,3 +1,8 @@ +{ lib }: +let + dashLib = import ../lib.nix { inherit lib; }; + inherit (dashLib) entities ids; +in { type = "vertical-stack"; cards = [ @@ -13,11 +18,11 @@ badges = [ { type = "entity"; - entity = "switch.adguard_home_protection"; + entity = entities.adguardProtection; } { type = "entity"; - entity = "sensor.adguard_home_average_processing_speed"; + entity = entities.adguardSpeed; } ]; } @@ -30,10 +35,10 @@ conditions = [ ]; card = { type = "custom:button-card"; - entity = "sensor.uptimekuma_uptime_racci_dev"; + entity = entities.sensors.uptimekuma; icon = "mdi:devices"; name = "Monitored"; - label = "[[[return states[\"sensor.uptimekuma_uptime_racci_dev\"].attributes.monitored]]]"; + label = "[[[return states[\"${entities.sensors.uptimekuma}\"].attributes.monitored]]]"; template = "nav_button_state_small"; variables = { navigation_path = "server#monitored"; @@ -49,10 +54,10 @@ conditions = [ ]; card = { type = "custom:button-card"; - entity = "sensor.uptimekuma_uptime_racci_dev"; + entity = entities.sensors.uptimekuma; icon = "mdi:sort-clock-descending-outline"; name = "Uptime Kuma"; - label = "[[[return states[\"sensor.uptimekuma_uptime_racci_dev\"].attributes.monitors]]]"; + label = "[[[return states[\"${entities.sensors.uptimekuma}\"].attributes.monitors]]]"; template = "nav_button_small"; variables = { navigation_path = "#uptime"; @@ -71,7 +76,7 @@ { condition = "user"; users = [ - "3eea636aa3de4c7f9c662ad29c6e92e0" + ids.james "c82f30a396fb42a9a10514fd63d5aac7" ]; } diff --git a/hosts/server/nixcloud/home-assistant/dashboard/components/button-cards.nix b/hosts/server/nixcloud/home-assistant/dashboard/components/button-cards.nix new file mode 100644 index 000000000..92b9a2b8a --- /dev/null +++ b/hosts/server/nixcloud/home-assistant/dashboard/components/button-cards.nix @@ -0,0 +1,1682 @@ +{ lib, ... }: +with (import ../lib.nix { inherit lib; }); +{ + setup = { + state = [ + { + value = "unavailable"; + icon = "mdi:alert-circle-outline"; + styles = mkStyles { + state = { + text-decoration = "line-through"; + }; + label = { + text-decoration = "line-through"; + }; + }; + } + ]; + styles = mkStyles { + name = { + font-family = null; + }; + label = { + font-family = null; + }; + state = { + font-family = null; + }; + }; + }; + + nav_button_small = { + template = "setup"; + variables = { + navigation_path = "home"; + icon_on = entity.icon; + icon_off = entity.icon; + state_on = "on"; + state_off = "off"; + background_color_on = "var(--red)"; + background_color_off = "var(--green)"; + color_on = "var(--black)"; + color_off = "var(--black)"; + }; + type = "custom:button-card"; + inherit (entity) icon; + show_label = true; + tap_action = { + action = "navigate"; + navigation_path = "[[[ return variables.navigation_path ]]]"; + haptic = "success"; + }; + hold_action.action = "more-info"; + state = [ + (style.iconState "on") + (style.iconState "off") + ]; + custom_fields = { + icon2 = { + card = { + type = "custom:button-card"; + icon = "mdi:arrow-right-bold"; + styles = mkStyles { + card = { + background-color = "var(--contrast1)"; + width = "27px"; + height = "27px"; + }; + icon = { + width = "15px"; + color = "var(--contrast20)"; + }; + }; + }; + }; + }; + styles = mkStyles { + grid = { + grid-template-areas = ''" l l icon2 " " i n n "''; + grid-template-columns = "14px 1fr 1fr"; + grid-template-rows = "1fr min-content"; + }; + icon = { + width = "14px"; + margin-bottom = "5px"; + color = colourOnOff "black" "contrast20"; + }; + img_cell = { + justify-content = "flex-start"; + }; + name = { + justify-self = "start"; + font-size = "11px"; + margin-bottom = "2px"; + margin-left = "3px"; + color = colourOnOff "black" "contrast20"; + opacity = 0.7; + }; + card = { + height = "75px"; + background-color = colourOnOff "yellow" "contrast2"; + box-shadow = "none"; + border-radius = "24px"; + padding = "12px 0 12px 14px"; + z-index = 1; + }; + label = { + justify-self = "start"; + font-size = "20px"; + margin-top = "11px"; + font-weight = 500; + color = colourOnOff "black" "contrast20"; + }; + custom_fields = { + icon2 = { + margin-top = "-20px"; + justify-self = "end"; + align-self = "center"; + width = "24px"; + padding-right = "10px"; + color = colourOnOff "black" "contrast20"; + }; + }; + }; + }; + + nav_button_state_small = { + template = "setup"; + variables = { + navigation_path = "home"; + icon_on = entity.icon; + icon_off = entity.icon; + state_on = "on"; + state_off = "off"; + background_color_on = "var(--red)"; + background_color_off = "var(--green)"; + color_on = "var(--black)"; + color_off = "var(--black)"; + }; + type = "custom:button-card"; + inherit (entity) icon; + show_label = false; + show_state = true; + + tap_action = { + action = "navigate"; + navigation_path = "[[[ return variables.navigation_path ]]]"; + haptic = "success"; + }; + hold_action.action = "more-info"; + + state = [ + (style.iconState "on") + (style.iconState "off") + ]; + custom_fields = { + icon2 = { + card = { + type = "custom:button-card"; + icon = "mdi:arrow-right-bold"; + styles = mkStyles { + card = { + background-color = "var(--contrast1)"; + width = "27px"; + height = "27px"; + }; + icon = { + width = "15px"; + color = "var(--contrast20)"; + }; + }; + }; + }; + }; + styles = mkStyles { + grid = { + grid-template-areas = ''" s s icon2 " " i n n "''; + grid-template-columns = "14px 1fr 1fr"; + grid-template-rows = "1fr min-content"; + }; + icon = { + width = "14px"; + margin-bottom = "5px"; + color = colourOnOff "black" "contrast20"; + }; + img_cell = { + justify-content = "flex-start"; + }; + name = { + justify-self = "start"; + font-size = "11px"; + margin-bottom = "2px"; + margin-left = "3px"; + color = colourOnOff "black" "contrast20"; + opacity = 0.7; + }; + card = { + height = "75px"; + background-color = colourOnOff "yellow" "contrast2"; + box-shadow = "none"; + border-radius = "24px"; + padding = "12px 0 12px 14px"; + z-index = 1; + }; + state = { + justify-self = "start"; + font-size = "20px"; + margin-top = "11px"; + font-weight = 500; + color = colourOnOff "black" "contrast20"; + }; + custom_fields = { + icon2 = { + margin-top = "-20px"; + justify-self = "end"; + align-self = "center"; + width = "24px"; + padding-right = "10px"; + color = colourOnOff "black" "contrast20"; + }; + }; + }; + }; + + custom_card_alarm_bottom = { + template = "setup"; + entity = "sensor.wake_time_1"; + show_label = false; + name = "[[[ return states[\"sensor.time\"].state ]]]"; + show_state = false; + show_entity_picture = false; + icon = "mdi:alarm"; + styles = mkStyles { + grid = { + grid-template-areas = ''"i n snooze stop"''; + grid-template-columns = "min-content min-content 1fr 1fr"; + grid-template-rows = "min-content"; + column-gap = 15; + }; + icon = { + width = "60px"; + color = "var(--red)"; + animation = "alarm 0.8s ease infinite"; + }; + card = { + padding = "15px 15px 15px 15px"; + height = "180px"; + width = "100vw"; + overflow = "hidden"; + position = jsMatch { + value = ''states["${entities.inputBooleans.debugRounded}"].state''; + cases = [ + { + match = "on"; + ret = "'static'"; + } + ]; + default = "'fixed'"; + }; + margin = 0; + bottom = 0; + left = 0; + z-index = 2; + border-radius = "20px 20px 0px 0px"; + box-shadow = "rgba(14, 30, 37, 0.12) 0px 2px 4px 0px, rgba(14, 30, 37, 0.32) 0px 2px 16px 0px;"; + }; + img_cell = { + background = "none"; + border-radius = "10px"; + width = "50px"; + height = "50px"; + justify-self = "start"; + align-self = "start"; + left = "10px"; + }; + name = { + font-size = "28px"; + font-weight = 700; + justify-self = "start"; + align-self = "center"; + padding-left = "10px"; + }; + custom_fields = { + snooze = { + justify-self = "end"; + }; + stop = { + justify-self = "end"; + }; + }; + }; + extra_styles = '' + @keyframes alarm { + 0%, 80%, 100% { transform: translateY(0); } + 10% { transform: translateY(-2px) rotate(-8deg); } + 20% { transform: translateY(-2px) rotate(9deg); } + 30% { transform: translateY(-2px) rotate(-5deg); } + 40% { transform: translateY(-2px) rotate(4deg); } + 50% { transform: translateY(0); } + 60% { transform: translateY(-1.2px) } + } + ''; + custom_fields = { + snooze = { + card = { + type = "custom:button-card"; + icon = "mdi:alarm-snooze"; + entity = entities.inputBooleans.alarmSnoozed; + name = "Snooze"; + show_name = true; + show_icon = true; + tap_action = { + action = "toggle"; + }; + styles = mkStyles { + grid = { + grid-template-areas = "\"i n\""; + grid-template-columns = "min-content min-content"; + }; + card = { + padding = "4px"; + background = "var(--orange)"; + border-radius = "24px"; + width = "90px"; + }; + img_cell = { + justify-self = "start"; + width = "25px"; + height = "25px"; + background = "var(--contrast1)"; + border-radius = "100%"; + }; + name = { + font-size = "12px"; + color = "var(--black)"; + font-weight = 500; + justify-self = "start"; + align-self = "center"; + padding-left = "5px"; + }; + icon = { + width = "14px"; + color = "var(--orange)"; + align-self = "center"; + }; + }; + }; + }; + stop = { + card = { + type = "custom:button-card"; + icon = "mdi:alarm-off"; + entity = entities.inputBooleans.soundAlarmRunning; + name = "STOP"; + show_name = true; + show_icon = true; + tap_action.action = "toggle"; + styles = mkStyles { + grid = { + grid-template-areas = ''"i n"''; + grid-template-columns = "min-content min-content"; + }; + card = { + padding = "4px"; + background = "var(--red)"; + border-radius = "24px"; + width = "90px"; + margin-right = "5px"; + }; + img_cell = { + justify-self = "start"; + width = "25px"; + height = "25px"; + background = "var(--contrast1)"; + border-radius = "100%"; + }; + name = { + font-size = "12px"; + color = "var(--black)"; + font-weight = 500; + justify-self = "start"; + align-self = "center"; + padding-left = "5px"; + }; + icon = { + width = "14px"; + color = "var(--red)"; + align-self = "center"; + }; + }; + }; + }; + }; + }; + + custom_card_mediaplayer_bottom = { + template = "setup"; + show_label = true; + name = '' + [[[ + if (states[ entity.entity_id ].attributes.active_child == "media_player.playstation_5") { + return states["sensor.ps5_343_activity"].attributes.players + } else if (states[ entity.entity_id ].attributes.active_child == "media_player.steam_jlnbln") { + return 'jlnbln' + } else if (states[ entity.entity_id ].attributes.active_child == "media_player.plex_bedroom") { + return states[ entity.entity_id ].attributes.media_series_title + } else { + if (states[ entity.entity_id ].attributes.media_artist != null ) { + return states[ entity.entity_id ].attributes.media_artist + } else { + return "Streaming" + } + } + ]]] + ''; + label = "[[[ return states[ entity.entity_id ].attributes.media_title ]]]"; + show_state = false; + show_entity_picture = true; + entity_picture = '' + [[[ + if (states[ entity.entity_id ].attributes.active_child == "media_player.playstation_5") { + return states["sensor.ps5_343_activity"].attributes.title_image + } else if (states[ entity.entity_id ].attributes.active_child == "media_player.apple_tv_4k_2") { + if (states[ entity.entity_id ].attributes.media_artist == "Rocket League Esports") { + return '/local/images/rlesports.jpg'; + } else { + return states["media_player.apple_tv_4k_2"].attributes.entity_picture + } + } else if (states[ entity.entity_id ].attributes.active_child == "media_player.steam_jlnbln") { + return states["sensor.steam_76561197981585794"].attributes.game_image_main + } else if (states[ entity.entity_id ].attributes.active_child == "media_player.plex_bedroom") { + return states["media_player.plex_bedroom"].attributes.entity_picture + } else if (states[ entity.entity_id ].attributes.active_child == "media_player.music") { + if (states["media_player.music"].attributes.active_child == "media_player.bedroom_nest_music_assistant") { + return states["media_player.bedroom_speakers"].attributes.entity_picture + } else { + return states[ entity.entity_id ].attributes.entity_picture + } + } else if (states[ entity.entity_id ].attributes.active_child == "media_player.audiobook") { + if (states["input_select.audiobook"].state == "Harry Potter") { + if (states["input_number.hp_book"].state == 1.0) { + return "/local/images/HP_1.jpg"; + } else if (states["input_number.hp_book"].state == 2.0) { + return "/local/images/HP_2.jpg"; + } else if (states["input_number.hp_book"].state == 3.0) { + return "/local/images/HP_3.jpg"; + } else if (states["input_number.hp_book"].state == 4.0) { + return "/local/images/HP_4.jpg"; + } else if (states["input_number.hp_book"].state == 5.0) { + return "/local/images/HP_5.jpg"; + } else if (states["input_number.hp_book"].state == 6.0) { + return "/local/images/HP_6.jpg"; + } else if (states["input_number.hp_book"].state == 7.0) { + return "/local/images/HP_7.jpg"; + } + } + } else { + return states[ entity.entity_id ].attributes.entity_picture + } + ]]] + ''; + icon = '' + [[[ + const child = states[entity.entity_id].attributes.active_child; + if (child === "media_player.playstation_5") return 'mdi:sony-playstation'; + const app = states[entity.entity_id].attributes.app_name; + if (app === "YouTube") return 'mdi:youtube-tv'; + if (app === "Netflix") return 'mdi:netflix'; + if (app === "Plex") return 'mdi:plex'; + return 'mdi:television-guide'; + ]]] + ''; + styles = mkStyles { + grid = { + grid-template-areas = ''"i play_state button" "i l button" "i n button" "bar bar bar"''; + grid-template-columns = "min-content"; + grid-template-rows = "min-content min-content min-content"; + column-gap = 15; + }; + icon = { + width = 60; + height = 60; + color = "var(--red)"; + }; + card = { + padding = "15px 15px 15px 15px"; + height = 180; + width = "100vw"; + overflow = "hidden"; + position = jsMatch { + value = ''states["${entities.inputBooleans.debugRounded}"].state''; + cases = [ + { + match = "on"; + ret = "'static'"; + } + ]; + default = "'fixed'"; + }; + margin = 0; + bottom = 0; + left = 0; + z-index = 2; + border-radius = "20px 20px 0px 0px"; + box-shadow = "rgba(14, 30, 37, 0.12) 0px 2px 4px 0px, rgba(14, 30, 37, 0.32) 0px 2px 16px 0px"; + }; + img_cell = { + background = "none"; + border-radius = 10; + width = 60; + height = 60; + justify-self = "start"; + align-self = "start"; + left = 20; + }; + entity_picture = { + object-fit = "cover"; + }; + state = { + font-size = 10; + justify-self = "start"; + align-self = "start"; + color = "var(--contrast10)"; + padding-right = 25; + }; + name = { + font-size = 11; + justify-self = "start"; + align-self = "start"; + padding-right = 20; + animation = "my-animation 15s linear infinite"; + }; + label = { + justify-self = "start"; + align-self = "end"; + font-weight = 700; + padding-right = 20; + font-size = 13; + margin-top = "-5px"; + }; + custom_fields = { + button = { + justify-self = "end"; + align-self = "center"; + padding-right = 20; + padding-top = 7; + }; + play_state = { + font-size = 10; + justify-self = "start"; + align-self = "start"; + color = "var(--contrast14)"; + padding-right = 21; + padding-top = 10; + }; + progress = { + background-color = "var(--contrast10)"; + position = "absolute"; + top = "unset"; + bottom = 90; + left = 40; + height = 2; + width = "80%"; + }; + bar = { + background-color = "var(--green)"; + position = "absolute"; + bottom = 90; + left = 40; + top = "unset"; + height = 2; + z-index = 1; + transition = "1s ease-out"; + }; + }; + }; + custom_fields = { + button.card = { + type = "custom:button-card"; + icon = "mdi:play"; + entity = "[[[ return entity.entity_id ]]]"; + show_name = false; + tap_action = { + action = "call-service"; + service = "media_player.media_play_pause"; + target.entity_id = "[[[ return entity.entity_id ]]]"; + }; + styles = mkStyles { + card = { + overflow = "visible"; + background = "var(--contrast6)"; + border-radius = 10; + }; + icon = { + width = 17; + color = "var(--contrast16)"; + }; + img_cell = { + padding = 10; + width = 20; + }; + }; + state = [ + { + value = "playing"; + icon = jsMatch { + value = "states[entity.entity_id].attributes.active_child"; + cases = [ + { + match = "media_player.playstation_5"; + ret = "'mdi:sony-playstation'"; + } + { + match = "media_player.steam_jlnbln"; + ret = "'mdi:steam'"; + } + ]; + default = "'mdi:pause'"; + }; + } + ]; + }; + play_state = jsMatch { + value = "states[entity.entity_id].attributes.active_child"; + cases = [ + { + match = "media_player.spotify_james"; + ret = ''states[entity.entity_id].state + " - " + states[entity.entity_id].attributes.source''; + } + { + match = "media_player.playstation_5"; + ret = ''states[entity.entity_id].state + " - Playstation 5"''; + } + { + match = "media_player.audiobook"; + ret = ''states[entity.entity_id].state + " - Alarm Clock"''; + } + { + match = "media_player.plex_bedroom"; + ret = ''states[entity.entity_id].state + " - Bedroom TV"''; + } + { + match = "media_player.steam_jlnbln"; + ret = ''states[entity.entity_id].state + " - Steam"''; + } + { + match = "media_player.apple_tv_4k_2"; + ret = ''states[entity.entity_id].state + " - " + states[entity.entity_id].attributes.app_name''; + } + ]; + default = ''states[entity.entity_id].state + " - " + states["${entities.sensors.musicRoom}"].state''; + }; + bar = '' + [[[ + if (entity.attributes.media_position !== undefined) { + setTimeout(() => { + let elt = this.shadowRoot, + card = elt.getElementById('card'), + container = elt.getElementById('container'), + bar = elt.getElementById('bar'); + if (elt && card && container && bar) { + card.insertBefore(bar, container); + function update() { + let mediaPositionUpdatedAt = entity.attributes.media_position_updated_at, + mediaPosition = entity.attributes.media_position, + mediaDuration = entity.attributes.media_duration, + mediaContentType = entity.attributes.media_content_type; + let percentage = entity.state === 'paused' + ? (mediaPosition / mediaDuration * 80) + : entity.state === 'playing' + ? (((Date.now() / 1000) - (new Date(mediaPositionUpdatedAt).getTime() / 1000) + mediaPosition) / mediaDuration * 80) + : 0; + bar.style.width = percentage.toFixed(1) + '%'; + requestAnimationFrame(update); + } + requestAnimationFrame(update); + } + }, 0); + return ' ';} + ]]] + ''; + }; + }; + + custom_card_mediaplayer_music = { + template = "setup"; + show_label = true; + show_name = true; + show_entity_picture = true; + entity_picture = '' + [[[ + if (entity.attributes.entity_picture == undefined) { + return '/local/images/abstract-gif-colors.gif'; + } else { + return entity.attributes.entity_picture; + } + ]]] + ''; + name = "[[[ return entity.attributes.media_title || 'No media' ]]]"; + label = "[[[ return entity.attributes.media_artist || '' ]]]"; + tap_action.action = "more-info"; + styles = mkStyles { + grid = { + grid-template-areas = ''"i play_state icon1" "i l l" "i n n" "i buttons buttons" "bar bar bar"''; + grid-template-columns = "min-content 1fr min-content"; + grid-template-rows = "min-content min-content min-content min-content min-content"; + column-gap = "10px"; + }; + card = { + border-radius = "24px"; + background-color = "var(--contrast2)"; + box-shadow = "none"; + padding = "15px"; + overflow = "visible"; + }; + entity_picture = { + border-radius = "12px"; + width = "80px"; + height = "80px"; + object-fit = "cover"; + }; + img_cell = { + width = "80px"; + height = "80px"; + }; + name = { + justify-self = "start"; + font-size = "14px"; + font-weight = 700; + color = "var(--contrast20)"; + white-space = "nowrap"; + overflow = "hidden"; + text-overflow = "ellipsis"; + max-width = "100%"; + }; + label = { + justify-self = "start"; + font-size = "12px"; + font-weight = 500; + color = "var(--contrast14)"; + }; + custom_fields = { + play_state = { + justify-self = "start"; + font-size = "10px"; + font-weight = 500; + color = "var(--contrast10)"; + padding-top = "5px"; + }; + icon1 = { + justify-self = "end"; + color = "var(--contrast14)"; + }; + buttons = { + justify-self = "start"; + margin-top = "5px"; + }; + progress = { + background-color = "var(--contrast6)"; + position = "absolute"; + bottom = "15px"; + left = "15px"; + height = "3px"; + width = "calc(100% - 30px)"; + border-radius = "2px"; + }; + bar = { + background-color = "var(--green)"; + position = "absolute"; + bottom = "15px"; + left = "15px"; + height = "3px"; + z-index = 1; + border-radius = "2px"; + transition = "1s ease-out"; + }; + }; + }; + custom_fields = { + play_state = jsMatch { + value = "entity.state"; + cases = [ + { + match = "playing"; + ret = "'Playing'"; + } + { + match = "paused"; + ret = "'Paused'"; + } + ]; + default = "entity.state"; + }; + icon1 = "[[[ return variables.icon_1 || '' ]]]"; + progress = " "; + bar = '' + [[[ + if (entity.attributes.media_position !== undefined) { + setTimeout(() => { + let elt = this.shadowRoot, + card = elt.getElementById('card'), + container = elt.getElementById('container'), + bar = elt.getElementById('bar'); + if (elt && card && container && bar) { + card.insertBefore(bar, container); + function update() { + let mediaPositionUpdatedAt = entity.attributes.media_position_updated_at, + mediaPosition = entity.attributes.media_position, + mediaDuration = entity.attributes.media_duration; + let percentage = entity.state === 'paused' + ? (mediaPosition / mediaDuration * 93) + : entity.state === 'playing' + ? (((Date.now() / 1000) - (new Date(mediaPositionUpdatedAt).getTime() / 1000) + mediaPosition) / mediaDuration * 93) + : 0; + bar.style.width = percentage.toFixed(1) + '%'; + requestAnimationFrame(update); + } + requestAnimationFrame(update); + } + }, 0); + return ' '; + } + ]]] + ''; + buttons.card = { + type = "custom:button-card"; + styles = mkStyles { + card = { + background = "none"; + box-shadow = "none"; + padding = "0"; + }; + grid = { + grid-template-areas = ''"prev play_pause next"''; + grid-template-columns = "1fr 1fr 1fr"; + column-gap = "8px"; + }; + }; + custom_fields = { + prev.card = { + type = "custom:button-card"; + icon = "mdi:skip-previous"; + entity = "[[[ return entity.entity_id ]]]"; + show_name = false; + tap_action = { + action = "call-service"; + service = "media_player.media_previous_track"; + target.entity_id = "[[[ return entity.entity_id ]]]"; + }; + styles = mkStyles { + card = { + background = "var(--contrast4)"; + border-radius = "10px"; + padding = "8px"; + }; + icon = { + width = "16px"; + color = "var(--contrast16)"; + }; + }; + }; + play_pause.card = { + type = "custom:button-card"; + icon = jsMatch { + value = "entity.state"; + cases = [ + { + match = "playing"; + ret = "'mdi:pause'"; + } + ]; + default = "'mdi:play'"; + }; + entity = "[[[ return entity.entity_id ]]]"; + show_name = false; + tap_action = { + action = "call-service"; + service = "media_player.media_play_pause"; + target.entity_id = "[[[ return entity.entity_id ]]]"; + }; + styles = mkStyles { + card = { + background = "var(--green)"; + border-radius = "10px"; + padding = "8px"; + }; + icon = { + width = "18px"; + color = "var(--black)"; + }; + }; + }; + next.card = { + type = "custom:button-card"; + icon = "mdi:skip-next"; + entity = "[[[ return entity.entity_id ]]]"; + show_name = false; + tap_action = { + action = "call-service"; + service = "media_player.media_next_track"; + target.entity_id = "[[[ return entity.entity_id ]]]"; + }; + styles = mkStyles { + card = { + background = "var(--contrast4)"; + border-radius = "10px"; + padding = "8px"; + }; + icon = { + width = "16px"; + color = "var(--contrast16)"; + }; + }; + }; + }; + }; + }; + variables = { + icon_1 = ""; + }; + }; + + custom_card_mediaplayer_tv = { + template = "setup"; + show_label = true; + show_name = true; + show_state = true; + show_entity_picture = true; + entity_picture = '' + [[[ + if (entity.attributes.entity_picture == undefined) { + return '/local/images/tv-placeholder.png'; + } else { + return entity.attributes.entity_picture; + } + ]]] + ''; + label = '' + [[[ + if (entity.attributes.app_name) return entity.attributes.app_name; + else return entity.state; + ]]] + ''; + name = '' + [[[ + if (entity.attributes.active_child == "media_player.playstation_5") { + return states["sensor.ps5_343_activity"].attributes.players || "Playstation 5"; + } else if (entity.attributes.active_child == "media_player.steam_jlnbln") { + return "Steam"; + } else if (entity.attributes.media_artist) { + return entity.attributes.media_artist; + } else if (entity.attributes.media_title) { + return entity.attributes.media_title; + } else { + return entity.attributes.friendly_name || "TV"; + } + ]]] + ''; + state_display = '' + [[[ + if (entity.attributes.media_title) return entity.attributes.media_title; + else return ""; + ]]] + ''; + tap_action.action = "more-info"; + styles = mkStyles { + grid = { + grid-template-areas = ''"app_icon l i" "s s s" "n n n"''; + grid-template-columns = "min-content 1fr min-content"; + grid-template-rows = "min-content min-content min-content"; + column-gap = "10px"; + }; + card = { + border-radius = "24px"; + background-color = "var(--contrast2)"; + box-shadow = "none"; + padding = "15px"; + overflow = "hidden"; + }; + entity_picture = { + border-radius = "8px"; + width = "40px"; + height = "40px"; + object-fit = "cover"; + }; + img_cell = { + width = "40px"; + height = "40px"; + display = "none"; + }; + icon = { + width = "24px"; + color = "var(--contrast14)"; + }; + label = { + justify-self = "start"; + align-self = "center"; + font-size = "12px"; + font-weight = 600; + color = "var(--contrast14)"; + }; + state = { + justify-self = "start"; + font-size = "14px"; + font-weight = 500; + color = "var(--contrast16)"; + margin-top = "5px"; + }; + name = { + justify-self = "start"; + font-size = "16px"; + font-weight = 700; + color = "var(--contrast20)"; + margin-top = "2px"; + white-space = "nowrap"; + overflow = "hidden"; + text-overflow = "ellipsis"; + }; + custom_fields = { + app_icon = { + width = "40px"; + height = "40px"; + border-radius = "8px"; + overflow = "hidden"; + }; + background_cover = { + position = "absolute"; + top = "0"; + left = "0"; + width = "100%"; + height = "100%"; + opacity = "0.15"; + z-index = "0"; + background-size = "cover"; + background-position = "center"; + }; + }; + }; + custom_fields = { + app_icon = '' + [[[ + if (entity.attributes.active_child == "media_player.playstation_5") { + return ''; + } else if (entity.attributes.active_child == "media_player.steam_jlnbln") { + return ''; + } else if (entity.attributes.active_child == "media_player.plex_bedroom") { + return ''; + } else if (entity.attributes.app_name == "YouTube") { + return ''; + } else if (entity.attributes.app_name == "Netflix") { + return ''; + } else { + return ''; + } + ]]] + ''; + background_cover = '' + [[[ + let bgImage = ""; + if (entity.attributes.active_child == "media_player.playstation_5") { + bgImage = states["sensor.ps5_343_activity"].attributes.title_image || ""; + } else if (entity.attributes.active_child == "media_player.steam_jlnbln") { + bgImage = states["sensor.steam_76561197981585794"].attributes.game_image_main || ""; + } else if (entity.attributes.entity_picture) { + bgImage = entity.attributes.entity_picture; + } + if (bgImage) { + return '
'; + } + return ""; + ]]] + ''; + }; + }; + + custom_card_user = { + template = "setup"; + show_name = true; + show_label = true; + show_icon = false; + show_entity_picture = false; + name = '' + [[[ + var hour = new Date().getHours(); + var greeting = ""; + if (hour >= 5 && hour < 12) greeting = "Good morning"; + else if (hour >= 12 && hour < 17) greeting = "Good afternoon"; + else if (hour >= 17 && hour < 21) greeting = "Good evening"; + else greeting = "Good night"; + var name = entity.attributes.friendly_name || "User"; + return greeting + ", " + name + "!"; + ]]] + ''; + label = jsMatch { + value = "entity.state"; + cases = [ + { + match = "home"; + ret = "'Welcome home'"; + } + ]; + default = "'You are away'"; + }; + tap_action.action = "more-info"; + styles = mkStyles { + grid = { + grid-template-areas = ''"pic n badge" "pic l badge" "one two three"''; + grid-template-columns = "min-content 1fr min-content"; + grid-template-rows = "min-content min-content min-content"; + column-gap = "10px"; + row-gap = "5px"; + }; + card = { + border-radius = "24px"; + background-color = "var(--contrast2)"; + box-shadow = "none"; + padding = "15px"; + }; + name = { + justify-self = "start"; + align-self = "end"; + font-size = "18px"; + font-weight = 700; + color = "var(--contrast20)"; + }; + label = { + justify-self = "start"; + align-self = "start"; + font-size = "12px"; + font-weight = 500; + color = "var(--contrast14)"; + }; + custom_fields = { + pic = { + width = "50px"; + height = "50px"; + border-radius = "50%"; + overflow = "hidden"; + }; + badge = { + justify-self = "end"; + align-self = "center"; + }; + one = { + justify-self = "start"; + margin-top = "10px"; + }; + two = { + justify-self = "center"; + margin-top = "10px"; + }; + three = { + justify-self = "end"; + margin-top = "10px"; + }; + }; + }; + custom_fields = { + pic = '' + [[[ + var pic = entity.attributes.entity_picture || ""; + if (pic) { + return ''; + } + return ''; + ]]] + ''; + badge = jsMatch { + value = "entity.state"; + cases = [ + { + match = "home"; + ret = ''""''; + } + ]; + default = ''""''; + }; + one = '' + [[[ + var james = states["person.james"]; + if (james) { + var pic = james.attributes.entity_picture || ""; + var home = james.state == "home"; + var opacity = home ? "1" : "0.4"; + if (pic) { + return ''; + } + } + return ""; + ]]] + ''; + two = '' + [[[ + var savannah = states["person.savannah"]; + if (savannah) { + var pic = savannah.attributes.entity_picture || ""; + var home = savannah.state == "home"; + var opacity = home ? "1" : "0.4"; + if (pic) { + return ''; + } + } + return ""; + ]]] + ''; + three = '' + [[[ + var poppy = states["person.poppy"]; + if (poppy) { + var pic = poppy.attributes.entity_picture || ""; + var home = poppy.state == "home"; + var opacity = home ? "1" : "0.4"; + if (pic) { + return ''; + } + } + return ""; + ]]] + ''; + }; + }; + + custom_card_climate = { + template = "setup"; + variables = { + name = "Climate"; + sensor = ""; + open = "Open"; + radius = "24px"; + }; + show_name = true; + show_label = true; + show_icon = true; + show_state = false; + icon = "mdi:thermometer"; + name = "[[[ return variables.name ]]]"; + label = '' + [[[ + if (entity.attributes.current_temperature !== undefined) { + return entity.attributes.current_temperature + "°C"; + } + return entity.state; + ]]] + ''; + tap_action.action = "more-info"; + styles = mkStyles { + grid = { + grid-template-areas = ''"i n window" "i l slider" "i humidity slider"''; + grid-template-columns = "min-content 1fr min-content"; + grid-template-rows = "min-content min-content min-content"; + column-gap = "10px"; + }; + card = { + border-radius = "[[[ return variables.radius ]]]"; + background-color = "var(--contrast2)"; + box-shadow = "none"; + padding = "15px"; + }; + icon = { + width = "40px"; + height = "40px"; + color = "var(--orange)"; + }; + name = { + justify-self = "start"; + align-self = "end"; + font-size = "14px"; + font-weight = 600; + color = "var(--contrast20)"; + }; + label = { + justify-self = "start"; + align-self = "center"; + font-size = "24px"; + font-weight = 700; + color = "var(--contrast20)"; + }; + custom_fields = { + window = { + justify-self = "end"; + align-self = "start"; + }; + humidity = { + justify-self = "start"; + align-self = "start"; + font-size = "12px"; + color = "var(--contrast14)"; + }; + slider = { + justify-self = "end"; + align-self = "center"; + }; + }; + }; + custom_fields = { + window = '' + [[[ + if (variables.sensor && states[variables.sensor]) { + if (states[variables.sensor].state == "on") { + return ''; + } + } + return ""; + ]]] + ''; + humidity = '' + [[[ + if (entity.attributes.current_humidity !== undefined) { + return ' ' + entity.attributes.current_humidity + '%'; + } + return ""; + ]]] + ''; + slider.card = { + type = "custom:button-card"; + styles = mkStyles { + card = { + background = "none"; + box-shadow = "none"; + padding = "0"; + }; + grid = { + grid-template-areas = ''"minus temp plus"''; + grid-template-columns = "1fr 1fr 1fr"; + column-gap = "5px"; + }; + }; + custom_fields = { + minus.card = { + type = "custom:button-card"; + icon = "mdi:minus"; + show_name = false; + tap_action = { + action = "call-service"; + service = "climate.set_temperature"; + target.entity_id = "[[[ return entity.entity_id ]]]"; + data.temperature = "[[[ return entity.attributes.temperature - 0.5 ]]]"; + }; + styles = mkStyles { + card = { + background = "var(--contrast4)"; + border-radius = "10px"; + padding = "8px"; + }; + icon = { + width = "16px"; + color = "var(--contrast16)"; + }; + }; + }; + temp = '' + [[[ + if (entity.attributes.temperature !== undefined) { + return entity.attributes.temperature + "°"; + } + return ""; + ]]] + ''; + plus.card = { + type = "custom:button-card"; + icon = "mdi:plus"; + show_name = false; + tap_action = { + action = "call-service"; + service = "climate.set_temperature"; + target.entity_id = "[[[ return entity.entity_id ]]]"; + data.temperature = "[[[ return entity.attributes.temperature + 0.5 ]]]"; + }; + styles = mkStyles { + card = { + background = "var(--contrast4)"; + border-radius = "10px"; + padding = "8px"; + }; + icon = { + width = "16px"; + color = "var(--contrast16)"; + }; + }; + }; + }; + }; + }; + }; + + custom_card_sensor_big = { + template = "setup"; + show_name = true; + show_label = false; + show_icon = true; + show_state = true; + tap_action.action = "more-info"; + state_display = '' + [[[ + var unit = entity.attributes.unit_of_measurement || ""; + return entity.state + ' ' + unit + ''; + ]]] + ''; + styles = mkStyles { + grid = { + grid-template-areas = ''"i" "n" "s"''; + grid-template-columns = "1fr"; + grid-template-rows = "1fr min-content min-content"; + }; + card = { + border-radius = "24px"; + background-color = "var(--contrast2)"; + box-shadow = "none"; + padding = "15px"; + height = "120px"; + }; + icon = { + width = "32px"; + color = "var(--contrast14)"; + justify-self = "start"; + }; + name = { + justify-self = "start"; + font-size = "12px"; + font-weight = 500; + color = "var(--contrast14)"; + margin-top = "auto"; + }; + state = { + justify-self = "start"; + font-size = "28px"; + font-weight = 700; + color = "var(--contrast20)"; + }; + }; + }; + + custom_card_sensor_medium = { + template = "setup"; + show_name = true; + show_label = false; + show_icon = true; + show_state = true; + tap_action.action = "more-info"; + state_display = '' + [[[ + var unit = entity.attributes.unit_of_measurement || ""; + return entity.state + ' ' + unit + ''; + ]]] + ''; + styles = mkStyles { + grid = { + grid-template-areas = ''"i n" "s s"''; + grid-template-columns = "min-content 1fr"; + grid-template-rows = "min-content min-content"; + column-gap = "8px"; + }; + card = { + border-radius = "24px"; + background-color = "var(--contrast2)"; + box-shadow = "none"; + padding = "12px"; + }; + icon = { + width = "24px"; + color = "var(--contrast14)"; + }; + name = { + justify-self = "start"; + align-self = "center"; + font-size = "12px"; + font-weight = 500; + color = "var(--contrast14)"; + }; + state = { + justify-self = "start"; + font-size = "20px"; + font-weight = 700; + color = "var(--contrast20)"; + margin-top = "5px"; + }; + }; + }; + + custom_card_sensor_small = { + template = "setup"; + show_name = true; + show_label = false; + show_icon = true; + show_state = true; + tap_action.action = "more-info"; + state_display = '' + [[[ + var unit = entity.attributes.unit_of_measurement || ""; + return entity.state + " " + unit; + ]]] + ''; + styles = mkStyles { + grid = { + grid-template-areas = ''"i n s"''; + grid-template-columns = "min-content 1fr min-content"; + grid-template-rows = "min-content"; + column-gap = "8px"; + }; + card = { + border-radius = "16px"; + background-color = "var(--contrast2)"; + box-shadow = "none"; + padding = "10px 12px"; + }; + icon = { + width = "20px"; + color = "var(--contrast14)"; + }; + name = { + justify-self = "start"; + align-self = "center"; + font-size = "12px"; + font-weight = 500; + color = "var(--contrast14)"; + }; + state = { + justify-self = "end"; + align-self = "center"; + font-size = "14px"; + font-weight = 600; + color = "var(--contrast20)"; + }; + }; + }; + + custom_card_timer_bottom = { + template = "setup"; + type = "custom:button-card"; + entity = '' + [[[ + if (states["sensor.kitchen_timer"].state == "on") { + return 'sensor.kitchen_timer' + } else if (states["sensor.living_room_timer"].state == "on") { + return 'sensor.living_room_timer' + } else if (states["sensor.office_timer"].state == "on") { + return 'sensor.office_timer' + } else if (states["sensor.bed_room_timer"].state == "on") { + return 'sensor.bed_room_timer' + } else if (states["sensor.bathroom_timer"].state == "on") { + return 'sensor.bathroom_timer' + } else { + return 'sensor.kitchen_timer' + } + ]]] + ''; + show_name = true; + show_icon = false; + show_label = false; + show_state = false; + tap_action.action = "more-info"; + hold_action.action = "more-info"; + custom_fields = { + bar = '' + [[[ + var color = "var(--green)"; + var state = 100 - states[ entity.entity_id ].attributes.remaining_perc; + if (state < 10) color = "var(--red)"; + else if (state < 50) color = "var(--yellow)"; + else if (state < 90) color = "var(--orange)"; + return ` +
+
+
+
` + ]]] + ''; + rem = { + card = { + type = "custom:button-card"; + entity = "[[[ return entity.entity_id ]]]"; + name = template.remainingTime; + show_icon = false; + styles = mkStyles { + card = { + width = "min"; + background = "none"; + overflow = "visible"; + }; + name = { + font-size = 14; + margin-top = 6; + font-weight = 600; + color = "var(--contrast20)"; + }; + }; + }; + }; + icon1 = ''[[[ return ''; ]]]''; + }; + styles = mkStyles { + grid = { + grid-template-areas = ''"rem icon1" "n icon2" "bar bar"''; + grid-template-rows = "min-content min-content min-content"; + grid-template-columns = "60% 40%"; + }; + card = { + padding = "15px 35px 15px 35px"; + background = "var(--contrast2)"; + height = template.stateIfElse { + entity = mediaPlayers.special.music_and_tv; + states = [ + "playing" + "paused" + ]; + ifTrue = "280px"; + ifFalse = "180px"; + }; + width = "100vw"; + overflow = "hidden"; + position = template.stateOnIfElse { + entity = entities.inputBooleans.debugRounded; + valueOn = "static"; + valueOff = "fixed"; + }; + margin = 0; + bottom = 0; + left = 0; + z-index = 1; + border-radius = "20px 20px 0px 0px"; + box-shadow = "rgba(14, 30, 37, 0.12) 0px 2px 4px 0px, rgba(14, 30, 37, 0.32) 0px 2px 16px 0px"; + }; + img_cell = { + position = "absolute"; + top = "20%"; + left = "40%"; + overflow = "visible"; + }; + label = { + text-align = "left"; + font-size = 18; + font-weight = 500; + justify-self = "start"; + align-self = "start"; + overflow = "visible"; + color = "var(--contrast20)"; + }; + name = { + text-align = "left"; + font-size = 12; + justify-self = "start"; + align-self = "start"; + overflow = "visible"; + color = "var(--contrast20)"; + }; + custom_fields = { + bar = { + justify-self = "start"; + align-self = "start"; + margin-top = 10; + width = "100%"; + height = template.stateOnIfElse { + entity = "entity.state"; + valueOn = "12px"; + valueOff = "0px"; + }; + background = "var(--contrast1)"; + border-radius = 24; + }; + rem = { + justify-self = "start"; + font-size = 14; + font-weight = 600; + align-self = "end"; + height = template.stateOnIfElse { + entity = "entity.state"; + valueOn = "27px"; + valueOff = "0px"; + }; + }; + icon1 = { + justify-self = "end"; + align-self = "start"; + width = 24; + color = "var(--contrast20)"; + }; + }; + + hold_action.action = "more-info"; + }; + }; +} diff --git a/hosts/server/nixcloud/home-assistant/dashboard/components/button-cards/default.nix b/hosts/server/nixcloud/home-assistant/dashboard/components/button-cards/default.nix new file mode 100644 index 000000000..634739f38 --- /dev/null +++ b/hosts/server/nixcloud/home-assistant/dashboard/components/button-cards/default.nix @@ -0,0 +1,6 @@ +{ + lib, +}: +{ + media-player = import ./media-player.nix { inherit lib; }; +} diff --git a/hosts/server/nixcloud/home-assistant/dashboard/components/button-cards/media-player.nix b/hosts/server/nixcloud/home-assistant/dashboard/components/button-cards/media-player.nix new file mode 100644 index 000000000..ddfe10581 --- /dev/null +++ b/hosts/server/nixcloud/home-assistant/dashboard/components/button-cards/media-player.nix @@ -0,0 +1,351 @@ +{ lib, ... }: +with (import ../../lib.nix { inherit lib; }); +{ + template = "setup"; + variables = { + room = "Select Room"; + icon_1 = ''''; + accent_color = "var(--green)"; + }; + show_entity_picture = true; + show_label = true; + entity_picture = '' + [[[ + if (entity.attributes.media_title == undefined || entity.attributes.entity_picture == undefined) { + return "/local/images/abstract-gif-colors.gif"; + } else { + return states[entity.entity_id].attributes.entity_picture; + } + ]]] + ''; + name = "[[[ return entity.attributes.media_title ]]]"; + label = "[[[ return entity.attributes.media_artist ]]]"; + tap_action.action = "none"; + styles = { + grid = { + grid-template-areas = ''"icon1 select_room play_state icon2" "i i i i" "n n n n" "l l l l" "album album album album" "buttons buttons buttons buttons"''; + grid-template-columns = "min-content 1fr 1fr min-content"; + # grid-template-rows = "min-content min-content min-content min-content min-content min-content"; + }; + card = { + background = "none"; + box-shadow = "none"; + border-radius = "0px"; + margin-top = "-10px"; + margin-bottom = "-10px"; + "--mdc-ripple-color" = "black"; + "--mdc-ripple-press-opacity" = 0; + }; + entity_picture = { + border-radius = "16px"; + margin-bottom = "35px"; + margin-top = "20px"; + object-fit = "cover"; + width = "200px"; + height = "200px"; + }; + name = { + justify-self = "center"; + font-weight = 600; + font-size = "15px"; + padding-top = "10px"; + width = "90%"; + }; + label = { + justify-self = "center"; + align-self = "start"; + color = "var(--contrast16)"; + font-weight = 500; + font-size = "12px"; + }; + custom_fields = { + icon1 = { + justify-self = "start"; + width = "24px"; + color = "var(--contrast20)"; + }; + icon2 = { + justify-self = "end"; + width = "24px"; + color = "var(--contrast20)"; + }; + select_room = { + font-size = "10px"; + justify-self = "start"; + align-self = "center"; + color = "var(--contrast16)"; + padding-left = "10px"; + }; + play_state = { + font-size = "10px"; + justify-self = "end"; + align-self = "center"; + color = "var(--contrast16)"; + padding-right = "10px"; + }; + album = { + justify-self = "center"; + align-self = "start"; + color = "var(--contrast16)"; + font-weight = 400; + font-size = "10px"; + margin-bottom = "5px"; + }; + progress = { + background-color = "var(--contrast4)"; + position = "absolute"; + top = "unset"; + bottom = "185px"; + left = "20%"; + height = "2px"; + width = "60%"; + border-radius = "4px"; + }; + bar = { + background-color = "[[[ return variables.accent_color ]]]"; + position = "absolute"; + top = "unset"; + bottom = "185px"; + left = "20%"; + height = "2px"; + z-index = 1; + border-radius = "4px"; + transition = "1s ease-out"; + }; + }; + }; + customFields = { + icon1 = "[[[ return variables.icon_1 ]]]"; + icon2 = ''[[[ return '' ]]]''; + select_room = "[[[ return variables.room ]]]"; + play_state = jsMatch { + value = "states[entity.entity_id].attributes.active_child"; + cases = [ + { + match = "media_player.spotify_james"; + ret = ''states[entity.entity_id].state + " - " + states[entity.entity_id].attributes.source''; + } + { + match = "media_player.apple_tv"; + ret = ''states[entity.entity_id].state + " - " + states[entity.entity_id].attributes.app_name''; + } + ]; + default = ''states[entity.entity_id].state + " - " + states[entity.entity_id].attributes.friendly_name''; + }; + album = mkButtonCard { + showLabel = true; + label = "[[[ return states[entity.entity_id].attributes.media_album_name ]]]"; + styles = { + card = { + background = "none"; + boxShadow = "none"; + borderRadius = 0; + padding = 0; + }; + label = { + justifySelf = "center"; + alignSelf = "start"; + color = "var(--contrast16)"; + fontWeight = 400; + fontSize = "10px"; + marginBottom = "5px"; + }; + }; + }; + progress = " "; + bar = '' + [[[ + if (entity.attributes.media_position !== undefined) { + setTimeout(() => { + let elt = this.shadowRoot, + card = elt.getElementById('card'), + container = elt.getElementById('container'), + bar = elt.getElementById('bar'); + if (elt && card && container && bar) { + card.insertBefore(bar, container); + function update() { + let mediaPositionUpdatedAt = entity.attributes.media_position_updated_at, + mediaPosition = entity.attributes.media_position, + mediaDuration = entity.attributes.media_duration; + let percentage = entity.state === 'paused' + ? (mediaPosition / mediaDuration * 60) + : entity.state === 'playing' + ? (((Date.now() / 1000) - (new Date(mediaPositionUpdatedAt).getTime() / 1000) + mediaPosition) / mediaDuration * 60) + : 0; + bar.style.width = percentage.toFixed(1) + '%'; + requestAnimationFrame(update); + } + requestAnimationFrame(update); + } + }, 0); + return ' '; + } + ]]] + ''; + buttons.card = { + type = "custom:button-card"; + styles = { + card = { + background = "none"; + box-shadow = "none"; + padding = "0"; + }; + grid = { + grid-template-areas = ''"loop prev play_pause next shuffle"''; + grid-template-columns = "1fr 1fr 1fr 1fr 1fr"; + column-gap = "10px"; + }; + }; + custom_fields = { + loop.card = { + type = "custom:button-card"; + icon = "mdi:repeat"; + entity = "[[[ return entity.entity_id ]]]"; + show_name = false; + tap_action = { + action = "call-service"; + service = "media_player.repeat_set"; + target.entity_id = "[[[ return entity.entity_id ]]]"; + data.repeat = jsMatch { + value = "entity.attributes.repeat"; + cases = [ + { + match = "off"; + ret = "'all'"; + } + { + match = "all"; + ret = "'one'"; + } + ]; + default = "'off'"; + }; + }; + styles = { + card = { + background = "var(--contrast4)"; + border-radius = "12px"; + padding = "10px"; + }; + icon = { + width = "20px"; + color = jsMatch { + value = "entity.attributes.repeat"; + cases = [ + { + match = "off"; + ret = "'var(--contrast10)'"; + } + ]; + default = "'var(--green)'"; + }; + }; + }; + }; + prev.card = { + type = "custom:button-card"; + icon = "mdi:skip-previous"; + entity = "[[[ return entity.entity_id ]]]"; + show_name = false; + tap_action = { + action = "call-service"; + service = "media_player.media_previous_track"; + target.entity_id = "[[[ return entity.entity_id ]]]"; + }; + styles = { + card = { + background = "var(--contrast4)"; + border-radius = "12px"; + padding = "10px"; + }; + icon = { + width = "20px"; + color = "var(--contrast16)"; + }; + }; + }; + play_pause.card = { + type = "custom:button-card"; + icon = jsMatch { + value = "entity.state"; + cases = [ + { + match = "playing"; + ret = "'mdi:pause'"; + } + ]; + default = "'mdi:play'"; + }; + entity = "[[[ return entity.entity_id ]]]"; + show_name = false; + tap_action = { + action = "call-service"; + service = "media_player.media_play_pause"; + target.entity_id = "[[[ return entity.entity_id ]]]"; + }; + styles = { + card = { + background = "var(--green)"; + border-radius = "12px"; + padding = "10px"; + }; + icon = { + width = "24px"; + color = "var(--black)"; + }; + }; + }; + next.card = { + type = "custom:button-card"; + icon = "mdi:skip-next"; + entity = "[[[ return entity.entity_id ]]]"; + show_name = false; + tap_action = { + action = "call-service"; + service = "media_player.media_next_track"; + target.entity_id = "[[[ return entity.entity_id ]]]"; + }; + styles = { + card = { + background = "var(--contrast4)"; + border-radius = "12px"; + padding = "10px"; + }; + icon = { + width = "20px"; + color = "var(--contrast16)"; + }; + }; + }; + shuffle.card = { + type = "custom:button-card"; + icon = "mdi:shuffle"; + entity = "[[[ return entity.entity_id ]]]"; + show_name = false; + tap_action = { + action = "call-service"; + service = "media_player.shuffle_set"; + target.entity_id = "[[[ return entity.entity_id ]]]"; + data.shuffle = "[[[ return !entity.attributes.shuffle ]]]"; + }; + styles = { + card = { + background = "var(--contrast4)"; + border-radius = "12px"; + padding = "10px"; + }; + icon = { + width = "20px"; + color = '' + [[[ + if (entity.attributes.shuffle) return "var(--green)"; + else return "var(--contrast10)"; + ]]] + ''; + }; + }; + }; + }; + }; + }; +} diff --git a/hosts/server/nixcloud/home-assistant/dashboard/components/button-cards/media_player.nix b/hosts/server/nixcloud/home-assistant/dashboard/components/button-cards/media_player.nix deleted file mode 100644 index d56e25421..000000000 --- a/hosts/server/nixcloud/home-assistant/dashboard/components/button-cards/media_player.nix +++ /dev/null @@ -1,47 +0,0 @@ -{ lib, ... }: -with (import ../lib.nix { inherit lib; }); -{ - type = "custom:button-card"; - template = "setup"; - variables = { - room = entity.name; - icon_1 = ""; - accent_color = "var(--green)"; - }; - show_entity_picture = true; - show_name = true; - show_state = true; - name = "[[[ return (variables.room && variables.room.length > 0) ? variables.room : (entity.attributes.friendly_name || 'Player') ]]]"; - state = [ - { - value = "playing"; - icon = "mdi:play-circle-outline"; - } - { - value = "paused"; - icon = "mdi:pause-circle-outline"; - } - ]; - tap_action.action = "more-info"; - styles = { - card = { - border-radius = "12px"; - background-color = "var(--contrast2)"; - box-shadow = "none"; - padding = "12px"; - }; - icon.color = "var(--black)"; - name = { - justify-self = "start"; - font-size = 14; - font-weight = 600; - color = "var(--contrast14)"; - }; - state = { - justify-self = "start"; - font-size = 12; - font-weight = 500; - color = "var(--contrast20)"; - }; - }; -} diff --git a/hosts/server/nixcloud/home-assistant/dashboard/templates/decluttering.nix b/hosts/server/nixcloud/home-assistant/dashboard/components/decluttering.nix similarity index 100% rename from hosts/server/nixcloud/home-assistant/dashboard/templates/decluttering.nix rename to hosts/server/nixcloud/home-assistant/dashboard/components/decluttering.nix diff --git a/hosts/server/nixcloud/home-assistant/dashboard/components/navbar.nix b/hosts/server/nixcloud/home-assistant/dashboard/components/navbar.nix index 4cbab4b6a..4c2d26aa4 100644 --- a/hosts/server/nixcloud/home-assistant/dashboard/components/navbar.nix +++ b/hosts/server/nixcloud/home-assistant/dashboard/components/navbar.nix @@ -1,6 +1,11 @@ { + lib, basePath ? "/lovelace", }: +let + dashLib = import ../lib.nix { inherit lib; }; + inherit (dashLib) entities mediaPlayers; +in { type = "custom:navbar-card"; @@ -69,13 +74,14 @@ url = "${basePath}/music"; label = "Music"; badge = { - show = "[[[ return states['media_player.music'].state === 'playing' ]]]"; + show = "[[[ return states['${mediaPlayers.special.music}'].state === 'playing' ]]]"; color = "var(--green-color)"; }; hidden = '' [[[ - if (states['media_player.music'].state == "playing") return false; - else return false; + if (states['${mediaPlayers.special.music}'].state == "playing") return true; + else if (states['remote.apple_tv'].state == "on") return false; + else return true; ]]] ''; hold_action.action = "open-popup"; @@ -97,12 +103,12 @@ url = "${basePath}/home#remote"; label = "TV"; badge = { - show = "[[[ return states['media_player.apple_tv'].state === 'playing' ]]]"; + show = "[[[ return states['${mediaPlayers.special.apple_tv}'].state === 'playing' ]]]"; color = "var(--green-color)"; }; hidden = '' [[[ - if (states['media_player.music'].state == "playing") return true; + if (states['${mediaPlayers.special.music}'].state == "playing") return true; else if (states['remote.apple_tv'].state == "on") return false; else return true; ]]] @@ -136,7 +142,7 @@ selected = "[[[ return location.hash == '#james'; ]]]"; hidden = ''[[[ return user.name != "James" ]]]''; badge = { - show = "[[[ return states['input_boolean.debug_rounded'].state === 'on' ]]]"; + show = "[[[ return states['${entities.inputBooleans.debugRounded}'].state === 'on' ]]]"; color = "var(--red-color)"; }; } @@ -153,9 +159,9 @@ badge = { show = '' [[[ - if (states['binary_sensor.monitored_entities'].state == "on") return true; - else if (states['binary_sensor.home_assistant_update'].state == "on") return true; - else if (states['binary_sensor.battery_health_attention'].state == "on") return true; + if (states['${entities.binarySensors.monitoredEntities}'].state == "on") return true; + else if (states['${entities.binarySensors.homeAssistantUpdate}'].state == "on") return true; + else if (states['${entities.binarySensors.batteryHealthAttention}'].state == "on") return true; else return false; ]]] ''; @@ -172,8 +178,8 @@ color = "var(--red-color)"; show = '' [[[ - if (states['binary_sensor.monitored_entities'].state == "on") return true; - else if (states['binary_sensor.battery_health_attention'].state == "on") return true; + if (states['${entities.binarySensors.monitoredEntities}'].state == "on") return true; + else if (states['${entities.binarySensors.batteryHealthAttention}'].state == "on") return true; else return false; ]]] ''; diff --git a/hosts/server/nixcloud/home-assistant/dashboard/components/templates/popup.nix b/hosts/server/nixcloud/home-assistant/dashboard/components/templates/popup.nix new file mode 100644 index 000000000..b57058f9d --- /dev/null +++ b/hosts/server/nixcloud/home-assistant/dashboard/components/templates/popup.nix @@ -0,0 +1,15 @@ +{ lib, ... }: +with (import ../../lib.nix { inherit lib; }); +{ + popup = + { + location, + }: + { + type = "custom:bubble-card"; + card_type = "pop-up"; + hash = "#${location}"; + button_type = "name"; + + }; +} diff --git a/hosts/server/nixcloud/home-assistant/dashboard/default.nix b/hosts/server/nixcloud/home-assistant/dashboard/default.nix index bf51de753..69962ff60 100644 --- a/hosts/server/nixcloud/home-assistant/dashboard/default.nix +++ b/hosts/server/nixcloud/home-assistant/dashboard/default.nix @@ -1,9 +1,9 @@ { lib, ... }: { services.home-assistant.lovelaceConfig = { - kiosk_mode = import ./kiosk-mode.nix; - decluttering_templates = import ./templates/decluttering.nix { inherit lib; }; - button_card_templates = import ./templates/button-cards.nix { inherit lib; }; + kiosk_mode = import ./kiosk-mode.nix { inherit lib; }; + decluttering_templates = import ./components/decluttering.nix { inherit lib; }; + button_card_templates = import ./components/button-cards { inherit lib; }; views = [ (import ./views/home.nix { inherit lib; }) (import ./views/music.nix { inherit lib; }) diff --git a/hosts/server/nixcloud/home-assistant/dashboard/kiosk-mode.nix b/hosts/server/nixcloud/home-assistant/dashboard/kiosk-mode.nix index c6d6868e7..74db23ece 100644 --- a/hosts/server/nixcloud/home-assistant/dashboard/kiosk-mode.nix +++ b/hosts/server/nixcloud/home-assistant/dashboard/kiosk-mode.nix @@ -1,3 +1,8 @@ +{ lib }: +let + dashLib = import ./lib.nix { inherit lib; }; + inherit (dashLib) entities; +in { non_admin_settings = { hide_header = true; @@ -5,11 +10,11 @@ }; entity_settings = [ { - entity."input_boolean.debug_rounded" = "on"; + entity.${entities.inputBooleans.debugRounded} = "on"; hide_header = false; } { - entity."input_boolean.debug_rounded" = "off"; + entity.${entities.inputBooleans.debugRounded} = "off"; hide_header = true; } ]; diff --git a/hosts/server/nixcloud/home-assistant/dashboard/lib.nix b/hosts/server/nixcloud/home-assistant/dashboard/lib.nix index d01da6710..3804101e8 100644 --- a/hosts/server/nixcloud/home-assistant/dashboard/lib.nix +++ b/hosts/server/nixcloud/home-assistant/dashboard/lib.nix @@ -1,10 +1,290 @@ { lib }: +let + inherit (lib) nameValuePair mapAttrs'; + inherit (lib.mine.strings) toSnakeCase; +in rec { + # User IDs for Home Assistant user conditions ids = { james = "3eea636aa3de4c7f9c662ad29c6e92e0"; savannah = "82def695e9504f63b1eb09150073737d"; }; + # User display names for UI labels + users = { + james = "James"; + savannah = "Savannah"; + }; + + # Centralized entity definitions to avoid duplication + entities = { + # Sensors + sensors = { + uptimekuma = "sensor.uptimekuma_uptime_racci_dev"; + musicRoom = "sensor.music_room"; + musicAssistantPlaying = "sensor.music_assistant_playing_devices"; + timeOfDay = "sensor.time_of_day"; + time = "sensor.time"; + windowOpenCount = "sensor.window_open_count"; + lightsOnCount = "sensor.lights_on_count"; + choresOnCount = "sensor.chores_on_count"; + batteryHealthAttention = "sensor.battery_health_attention"; + batteryLevelAttention = "sensor.battery_level_attention"; + monitoredEntities = "sensor.monitored_entities"; + powerConsumption = "sensor.power_consumption"; + homeAssistantUpdate = "sensor.home_assistant_update"; + # Timers + kitchenTimer = "sensor.kitchen_timer"; + livingRoomTimer = "sensor.living_room_timer"; + bedroomTimer = "sensor.bed_room_timer"; + bathroomTimer = "sensor.bathroom_timer"; + officeTimer = "sensor.office_timer"; + # 3D Printer + printerState = "sensor.k1c_276e_current_print_state"; + printerProgress = "sensor.k1c_276e_progress"; + # Gaming + ps5Activity = "sensor.ps5_343_activity"; + steamActivity = "sensor.steam_76561197981585794"; + # Alarm + wakeTime = "sensor.wake_time_1"; + isAlarmOn = "sensor.is_alarm_on"; + }; + + # Binary sensors + binarySensors = { + alexaTimerActive = "binary_sensor.alexa_timer_active"; + batteryHealthAttention = "binary_sensor.battery_health_attention"; + monitoredEntities = "binary_sensor.monitored_entities"; + isAlarmOn = "binary_sensor.is_alarm_on"; + homeAssistantUpdate = "binary_sensor.home_assistant_update"; + }; + + # Input booleans + inputBooleans = { + debugRounded = "input_boolean.debug_rounded"; + vacationMode = "input_boolean.vacation_mode"; + soundAlarmRunning = "input_boolean.sound_alarm_is_running"; + alarmSnoozed = "input_boolean.alarm_snoozed"; + notificationsJames = "input_boolean.notifications_james_phone"; + notificationsSavannah = "input_boolean.notifications_savannah_phone"; + musicPlayerIdleHelper = "input_boolean.music_player_idle_helper"; + }; + + # Persons + persons = { + james = "person.james"; + savannah = "person.savannah"; + }; + + # Other + alarm = "alarm_control_panel.security_system"; + adguardProtection = "switch.adguard_home_protection"; + adguardSpeed = "sensor.adguard_home_average_processing_speed"; + }; + + # Centralized media player entity definitions + mediaPlayers = { + # Room-based speakers + rooms = { + living_room = "media_player.living_room"; + kitchen = "media_player.kitchen"; + bed_room = "media_player.bed_room"; + bedroom_speakers = "media_player.bedroom_speakers"; + bedroom_nest = "media_player.bedroom_nest_music_assistant"; + bathroom = "media_player.bathroom"; + everywhere = "media_player.everywhere"; + spare_room = "media_player.spare_room"; + }; + + # Devices that also act as media players + special = { + apple_tv = "media_player.apple_tv"; + music = "media_player.music"; + music_and_tv = "media_player.music_and_tv"; + steam = "media_player.steam_jlnbln"; + }; + }; + + jsColours = { + yellow = "var(--yellow)"; + orange = "var(--orange)"; + black = "var(--black)"; + blue = "var(--blue)"; + contrast2 = "var(--contrast2)"; + contrast20 = "var(--contrast20)"; + }; + + # Helper to create a music room button card for the desktop view + mkMusicRoomButton = + { + room, # entity suffix (e.g., "bed_room") + icon, + script, # script to call on double tap + }: + { + type = "custom:button-card"; + entity = "media_player.${room}"; + inherit icon; + show_label = false; + show_state = true; + show_entity_picture = true; + styles = { + card = [ + { border-radius = "0px"; } + { box-shadow = "none"; } + { padding-right = "5px"; } + { background = "none"; } + ]; + grid = [ + { grid-template-areas = ''" 'i n' 'i s' "''; } + { grid-template-columns = "min-content"; } + { column-gap = "10px"; } + ]; + entity_picture = [ { border-radius = "100%"; } ]; + icon = [ { width = 20; } ]; + img_cell = [ { width = 20; } ]; + name = [ + { justify-self = "start"; } + { font-size = 10; } + { font-weight = 500; } + { color = "var(--contrast14)"; } + ]; + state = [ + { justify-self = "start"; } + { font-size = 15; } + { font-weight = 700; } + ]; + }; + tap_action.action = "more-info"; + double_tap_action = { + action = "perform-action"; + perform_action = script; + }; + }; + + # Helper to create a mobile music room transfer button + mkMusicRoomTransferButton = + { + room, # entity suffix + icon, + color, + targetEntity, # entity to transfer to + script, # script for double tap + }: + { + type = "custom:button-card"; + entity = "media_player.${room}"; + inherit icon; + show_name = false; + aspect_ratio = "1/1"; + state = [ + { + value = "playing"; + icon = "mdi:play-circle-outline"; + } + { + value = "paused"; + icon = "mdi:pause-circle-outline"; + } + ]; + tap_action = { + action = "call-service"; + service = "music_assistant.transfer_queue"; + data.source_player = jsMatch { + value = "states['sensor.music_room'].state"; + cases = [ + { + match = "Living Room"; + ret = "'${mediaPlayers.rooms.living_room}'"; + } + { + match = "Kitchen"; + ret = "'${mediaPlayers.rooms.kitchen}'"; + } + { + match = "Bathroom"; + ret = "'${mediaPlayers.rooms.bathroom}'"; + } + { + match = "Bedroom"; + ret = "'${mediaPlayers.rooms.bedroom_speakers}'"; + } + { + match = "Spare Room"; + ret = "'${mediaPlayers.rooms.spare_room}'"; + } + { + match = "Everywhere"; + ret = "'${mediaPlayers.rooms.everywhere}'"; + } + ]; + default = "''"; + }; + target.entity_id = targetEntity; + haptic = "success"; + }; + double_tap_action = { + action = "call-service"; + service = script; + haptic = "success"; + }; + styles.card = [ + { border-radius = "12px"; } + { background-color = "var(--${color})"; } + ]; + styles.icon = [ { color = "var(--black)"; } ]; + }; + + # Helper to create a conditional swipe card for a music room + mkMusicSwipeCard = + { + room, # display name (e.g., "Living Room") + entity, # full entity id + conditionEntity ? entity, # entity to check for playing/paused state + }: + { + type = "conditional"; + conditions = [ + { + condition = "numeric_state"; + entity = "sensor.music_assistant_playing_devices"; + above = 1; + } + { + condition = "state"; + entity = mediaPlayers.rooms.everywhere; + state_not = "playing"; + } + { + condition = "state"; + entity = entities.sensors.musicRoom; + state_not = room; + } + { + condition = "or"; + conditions = [ + { + condition = "state"; + entity = conditionEntity; + state = "paused"; + } + { + condition = "state"; + entity = conditionEntity; + state = "playing"; + } + ]; + } + ]; + card = { + type = "custom:button-card"; + inherit entity; + template = "media_player"; + variables.room = room; + variables.icon_1 = ''''; + }; + }; + entity = { id = "[[[ return entity.entity_id ]]]"; icon = "[[[ return entity.attributes.icon ]]]"; @@ -66,11 +346,11 @@ rec { ]; timerActive = condState { - entity = "binary_sensor.alexa_timer_active"; + entity = entities.binarySensors.alexaTimerActive; state = "on"; }; alarmActive = condState { - entity = "input_boolean.sound_alarm_is_running"; + entity = entities.inputBooleans.soundAlarmRunning; state = "on"; }; }; @@ -83,6 +363,18 @@ rec { ]]] ''; + getState = entity: '' + [[[ + return states[ "${entity}" ].state; + ]]] + ''; + + returnIcon = icon: '' + [[[ + return ''; + ]]] + ''; + stateOnIfElse = { entity ? "entity.state", @@ -128,23 +420,91 @@ rec { value = "[[[ return variables.state_${state} ]]]"; icon = "[[[ return variables.icon_${state} ]]]"; styles = { - card.background-color = "[[[ return variables.background_color_${state} ]]]"; - name.color = "[[[ return variables.color_${state} ]]]"; - label.color = "[[[ return variables.color_${state} ]]]"; - icon.color = "[[[ return variables.color_${state} ]]]"; + card = [ { background-color = "[[[ return variables.background_color_${state} ]]]"; } ]; + name = [ { color = "[[[ return variables.color_${state} ]]]"; } ]; + label = [ { color = "[[[ return variables.color_${state} ]]]"; } ]; + icon = [ { color = "[[[ return variables.color_${state} ]]]"; } ]; }; }; }; compact = attrs: lib.filterAttrs (_: v: v != null) attrs; + parseConvertCompact = + attrs: attrs |> compact |> mapAttrs' (name: value: nameValuePair (toSnakeCase name) value); + + # Convert a style attrset to an array of single-key objects for button-card + # { a = 1; b = 2; } -> [ { a = 1; } { b = 2; } ] + styleArray = attrs: lib.mapAttrsToList (n: v: { ${n} = v; }) attrs; + + # Convert a full styles block where each key (card, icon, etc.) needs to be an array + # Recursively handles nested custom_fields + mkStyles = + styles: + lib.mapAttrs ( + name: value: + if name == "custom_fields" then + # custom_fields is nested - each field's styles should also be arrays + lib.mapAttrs (_: styleArray) value + else if !lib.isAttrs value then + # Not an attrset, leave as-is + value + else + styleArray value + ) styles; + colourOnOff = on: off: '' [[[ - if (entity.state == "on") return "var(--${on})"; + if (entity && entity.state == "on") return "var(--${on})"; else return "var(--${off})"; ]]] ''; + # Generate a JavaScript template with match/switch-like semantics + # Usage: + # jsMatch { + # value = "entity.state"; # or "states['sensor.foo'].state" + # cases = [ + # { match = "on"; ret = "'green'"; } + # { match = "off"; ret = "'red'"; } + # { match = [ "unavailable" "unknown" ]; ret = "'gray'"; } # multiple values + # ]; + # default = "'black'"; # optional, defaults to "null" + # } + # Produces: + # [[[ + # const v = entity.state; + # if (v === "on") return 'green'; + # if (v === "off") return 'red'; + # if (v === "unavailable" || v === "unknown") return 'gray'; + # return 'black'; + # ]]] + jsMatch = + { + value, + cases, + default ? "null", + }: + let + mkCondition = + m: + if builtins.isList m then + builtins.concatStringsSep " || " (map (w: ''v === "${w}"'') m) + else + ''v === "${m}"''; + + mkCase = { match, ret, ... }: "if (${mkCondition match}) return ${ret};"; + + caseLines = map mkCase cases; + in + '' + [[[ + const v = ${value}; + ${builtins.concatStringsSep "\n " caseLines} + return ${default}; + ]]] + ''; + condScreen = media_query: { condition = "screen"; inherit media_query; @@ -156,7 +516,7 @@ rec { state ? null, state_not ? null, }: - compact { + parseConvertCompact { condition = "state"; inherit entity state state_not; }; @@ -167,7 +527,7 @@ rec { above ? null, below ? null, }: - compact { + parseConvertCompact { condition = "numeric_state"; inherit entity above below; }; @@ -225,7 +585,7 @@ rec { type ? "sections", max_columns ? null, }: - compact { + parseConvertCompact { inherit title header @@ -253,7 +613,7 @@ rec { show_entity_picture ? null, name ? null, }: - compact { + lib.warn "Convert to mkEntity, shit stain" (parseConvertCompact { type = "entity"; inherit entity @@ -267,6 +627,41 @@ rec { show_entity_picture name ; + }); + + mkEntity = + { + entity, + name ? null, + icon ? null, + color ? null, + iconColor ? null, + visibility ? null, + contentInfo ? null, + stateContent ? null, + showName ? null, + showState ? null, + showIcon ? null, + showEntityPicture ? null, + tapAction ? null, + }: + parseConvertCompact { + type = "entity"; + inherit + entity + name + icon + color + iconColor + visibility + contentInfo + stateContent + showName + showState + showIcon + showEntityPicture + tapAction + ; }; mkMushroomTemplateBadge = @@ -281,7 +676,7 @@ rec { tap_action ? null, visibility ? null, }: - compact { + parseConvertCompact { type = "custom:mushroom-template-badge"; inherit content @@ -296,17 +691,38 @@ rec { ; }; + mkMushroomChipsCard = + { + chips, + alignment ? "center", + }: + parseConvertCompact { + type = "custom:mushroom-chips-card"; + inherit chips alignment; + }; + + mkAction = + { + action, + haptic ? null, + }: + parseConvertCompact { + inherit action haptic; + }; + mkGridSection = { cards, + columns ? null, grid_options ? null, visibility ? null, column_span ? null, }: - compact { + parseConvertCompact { type = "grid"; inherit cards + columns grid_options visibility column_span @@ -356,21 +772,131 @@ rec { ; }; + # Convert attribute names from camelCase to snake_case for card definitions + # Does not recurse into nested attrsets + convertNames = + attrsList: + attrsList |> map (_attrs: mapAttrs' (name: value: nameValuePair (toSnakeCase name) value)); + + mkBubbleCard = + { + name ? null, + entity ? null, + icon ? null, + hash ? null, + + cardType, + subButton ? [ ], + showState ? null, + showName ? null, + showIcon ? null, + scrollingEffect ? null, + + tapAction ? null, + doubleTapAction ? null, + holdAction ? null, + buttonAction ? null, + + openAction ? null, + closeAction ? null, + + buttonType ? "name", + cardLayout ? "large", + hideBackdrop ? null, + styles ? null, + }: + parseConvertCompact { + type = "custom:bubble-card"; + inherit + name + entity + icon + hash + cardType + subButton + showState + showName + showIcon + scrollingEffect + tapAction + doubleTapAction + holdAction + buttonAction + openAction + closeAction + buttonType + cardLayout + hideBackdrop + styles + ; + }; + + mkButtonCard = + { + entity ? null, + name ? null, + label ? null, + icon ? null, + template ? null, + + showName ? null, + showIcon ? null, + showLabel ? null, + showState ? null, + + tapAction ? null, + holdAction ? null, + + variables ? { }, + styles ? { }, + customFields ? { }, + }: + parseConvertCompact { + type = "custom:button-card"; + inherit + entity + name + label + icon + template + showName + showIcon + showLabel + showState + tapAction + holdAction + styles + customFields + ; + variables = parseConvertCompact variables; + }; + + mkSwipeCard = + { + cardWidth ? null, + parameters ? null, + cards ? [ ], + }: + parseConvertCompact { + type = "custom:swipe-card"; + inherit cardWidth parameters cards; + }; + mkDeclutteringCard = { template, variables ? null, }: - compact { + parseConvertCompact { type = "custom:decluttering-card"; inherit template variables; }; standardBadges = [ - (mkBadgeEntity { + (mkEntity { entity = "input_boolean.vacation_mode"; color = "orange"; - tap_action.action = "toggle"; + tapAction.action = "toggle"; visibility = [ (condState { entity = "input_boolean.vacation_mode"; @@ -379,10 +905,10 @@ rec { ]; }) - (mkBadgeEntity { + (mkEntity { entity = "input_boolean.notifications_james_phone"; color = "primary"; - tap_action.action = "toggle"; + tapAction.action = "toggle"; visibility = [ (condState { entity = "input_boolean.notifications_james_phone"; @@ -392,12 +918,12 @@ rec { ]; }) - (mkBadgeEntity { + (mkEntity { entity = "input_boolean.notifications_savannah_phone"; color = "primary"; icon = "mdi:cellphone-message-off"; - show_entity_picture = false; - tap_action.action = "toggle"; + showEntityPicture = false; + tapAction.action = "toggle"; visibility = [ (condState { entity = "input_boolean.notifications_savannah_phone"; @@ -407,13 +933,13 @@ rec { ]; }) - (mkBadgeEntity { + (mkEntity { entity = "sensor.chores_on_count"; color = "yellow"; - show_state = true; - tap_action = { + showState = true; + tapAction = { action = "navigate"; - navigation_path = "home#chores"; + navigationPath = "home#chores"; }; visibility = [ (condNumericState { @@ -423,9 +949,9 @@ rec { ]; }) - (mkBadgeEntity { + (mkEntity { entity = "sensor.k1c_276e_progress"; - show_state = true; + showState = true; icon = "mdi:printer-3d"; color = "orange"; visibility = [ @@ -435,9 +961,9 @@ rec { }) (condUser [ "3eea636aa3de4c7f9c662ad29c6e92e0" ]) ]; - tap_action = { + tapAction = { action = "navigate"; - navigation_path = "server#3d-printer"; + navigationPath = "server#3d-printer"; }; }) diff --git a/hosts/server/nixcloud/home-assistant/dashboard/templates/button-cards.nix b/hosts/server/nixcloud/home-assistant/dashboard/templates/button-cards.nix deleted file mode 100644 index 6c4f69ade..000000000 --- a/hosts/server/nixcloud/home-assistant/dashboard/templates/button-cards.nix +++ /dev/null @@ -1,812 +0,0 @@ -{ lib, ... }: -with (import ../lib.nix { inherit lib; }); -{ - imports = [ - ../components/button-cards/media_player.nix - ]; - - setup = { - state = [ - { - value = "unavailable"; - icon = "mdi:alert-circle-outline"; - styles = { - state.text-decoration = "line-through"; - label.text-decoration = "line-through"; - }; - } - ]; - styles = { - name.font-family = null; - label.font-family = null; - state.font-family = null; - }; - }; - - nav_button_small = { - template = "setup"; - variables = { - navigation_path = "home"; - icon_on = entity.icon; - icon_off = entity.icon; - state_on = "on"; - state_off = "off"; - background_color_on = "var(--red)"; - background_color_off = "var(--green)"; - color_on = "var(--black)"; - color_off = "var(--black)"; - }; - type = "custom:button-card"; - inherit (entity) icon; - show_label = true; - tap_action = { - action = "navigate"; - navigation_path = "[[[ return variables.navigation_path ]]]"; - haptic = "success"; - }; - hold_action.action = "more-info"; - state = [ - (style.iconState "on") - (style.iconState "off") - ]; - custom_fields = { - icon2 = { - card = { - type = "custom:button-card"; - icon = "mdi:arrow-right-bold"; - styles = { - card = { - background-color = "var(--contrast1)"; - width = "27px"; - height = "27px"; - }; - icon = { - width = "15px"; - color = "var(--contrast20)"; - }; - }; - }; - }; - }; - styles = { - grid = { - grid-template-areas = ''" l l icon2 " " i n n "''; - grid-template-columns = "14px 1fr 1fr"; - grid-template-rows = "1fr min-content"; - }; - icon = { - width = "14px"; - margin-bottom = "5px"; - color = colourOnOff "black" "contrast20"; - }; - img_cell.justify-content = "flex-start"; - name = { - justify-self = "start"; - font-size = "11px"; - margin-bottom = "2px"; - margin-left = "3px"; - color = colourOnOff "black" "contrast20"; - opacity = 0.7; - }; - card = { - height = "75px"; - background-color = colourOnOff "yellow" "contrast2"; - box-shadow = "none"; - border-radius = "24px"; - padding = "12px 0 12px 14px"; - z-index = 1; - }; - label = { - justify-self = "start"; - font-size = "20px"; - margin-top = "11px"; - font-weight = 500; - color = colourOnOff "black" "contrast20"; - }; - custom_fields = { - icon2 = { - margin-top = "-20px"; - justify-self = "end"; - align-self = "center"; - width = "24px"; - padding-right = "10px"; - color = colourOnOff "black" "contrast20"; - }; - }; - }; - }; - - nav_button_state_small = { - template = "setup"; - variables = { - navigation_path = "home"; - icon_on = entity.icon; - icon_off = entity.icon; - state_on = "on"; - state_off = "off"; - background_color_on = "var(--red)"; - background_color_off = "var(--green)"; - color_on = "var(--black)"; - color_off = "var(--black)"; - }; - type = "custom:button-card"; - inherit (entity) icon; - show_label = false; - show_state = true; - - tap_action = { - action = "navigate"; - navigation_path = "[[[ return variables.navigation_path ]]]"; - haptic = "success"; - }; - hold_action.action = "more-info"; - - state = [ - (style.iconState "on") - (style.iconState "off") - ]; - custom_fields = { - icon2 = { - card = { - type = "custom:button-card"; - icon = "mdi:arrow-right-bold"; - styles = { - card = { - background-color = "var(--contrast1)"; - width = "27px"; - height = "27px"; - }; - icon = { - width = "15px"; - color = "var(--contrast20)"; - }; - }; - }; - }; - }; - styles = { - grid = { - grid-template-areas = ''" s s icon2 " " i n n "''; - grid-template-columns = "14px 1fr 1fr"; - grid-template-rows = "1fr min-content"; - }; - icon = { - width = "14px"; - margin-bottom = "5px"; - color = colourOnOff "black" "contrast20"; - }; - img_cell.justify-content = "flex-start"; - name = { - justify-self = "start"; - font-size = "11px"; - margin-bottom = "2px"; - margin-left = "3px"; - color = colourOnOff "black" "contrast20"; - opacity = 0.7; - }; - card = { - height = "75px"; - background-color = colourOnOff "yellow" "contrast2"; - box-shadow = "none"; - border-radius = "24px"; - padding = "12px 0 12px 14px"; - z-index = 1; - }; - state = { - justify-self = "start"; - font-size = "20px"; - margin-top = "11px"; - font-weight = 500; - color = colourOnOff "black" "contrast20"; - }; - custom_fields = { - icon2 = { - margin-top = "-20px"; - justify-self = "end"; - align-self = "center"; - width = "24px"; - padding-right = "10px"; - color = colourOnOff "black" "contrast20"; - }; - }; - }; - }; - - custom_card_alarm_bottom = { - template = "setup"; - entity = "sensor.wake_time_1"; - show_label = false; - name = "[[[ return states[\"sensor.time\"].state ]]]"; - show_state = false; - show_entity_picture = false; - icon = "mdi:alarm"; - styles = { - grid = { - grid-template-areas = ''"i n snooze stop"''; - grid-template-columns = "min-content min-content 1fr 1fr"; - grid-template-rows = "min-content"; - column-gap = 15; - }; - icon = { - width = "60px"; - color = "var(--red)"; - animation = "alarm 0.8s ease infinite"; - }; - card = { - padding = "15px 15px 15px 15px"; - height = "180px"; - width = "100vw"; - overflow = "hidden"; - position = '' - [[[ - if (states["input_boolean.debug_rounded"].state == "on") { - return 'static' - } else { - return 'fixed' - } - ]]] - ''; - margin = 0; - bottom = 0; - left = 0; - z-index = 2; - border-radius = "20px 20px 0px 0px"; - box-shadow = "rgba(14, 30, 37, 0.12) 0px 2px 4px 0px, rgba(14, 30, 37, 0.32) 0px 2px 16px 0px;"; - }; - img_cell = { - background = "none"; - border-radius = "10px"; - width = "50px"; - height = "50px"; - justify-self = "start"; - align-self = "start"; - left = "10px"; - }; - name = { - font-size = "28px"; - font-weight = 700; - justify-self = "start"; - align-self = "center"; - padding-left = "10px"; - }; - custom_fields = { - snooze.justify-self = "end"; - stop.justify-self = "end"; - }; - }; - extra_styles = '' - @keyframes alarm { - 0%, 80%, 100% { transform: translateY(0); } - 10% { transform: translateY(-2px) rotate(-8deg); } - 20% { transform: translateY(-2px) rotate(9deg); } - 30% { transform: translateY(-2px) rotate(-5deg); } - 40% { transform: translateY(-2px) rotate(4deg); } - 50% { transform: translateY(0); } - 60% { transform: translateY(-1.2px) } - } - ''; - custom_fields = { - snooze = { - card = { - type = "custom:button-card"; - icon = "mdi:alarm-snooze"; - entity = "input_boolean.alarm_snoozed"; - name = "Snooze"; - show_name = true; - show_icon = true; - tap_action = { - action = "toggle"; - }; - styles = { - grid = { - grid-template-areas = "\"i n\""; - grid-template-columns = "min-content min-content"; - }; - card = { - padding = "4px"; - background = "var(--orange)"; - border-radius = "24px"; - width = "90px"; - }; - img_cell = { - justify-self = "start"; - width = "25px"; - height = "25px"; - background = "var(--contrast1)"; - border-radius = "100%"; - }; - name = { - font-size = "12px"; - color = "var(--black)"; - font-weight = 500; - justify-self = "start"; - align-self = "center"; - padding-left = "5px"; - }; - icon = { - width = "14px"; - color = "var(--orange)"; - align-self = "center"; - }; - }; - }; - }; - stop = { - card = { - type = "custom:button-card"; - icon = "mdi:alarm-off"; - entity = "input_boolean.sound_alarm_is_running"; - name = "STOP"; - show_name = true; - show_icon = true; - tap_action.action = "toggle"; - styles = { - grid = { - grid-template-areas = ''"i n"''; - grid-template-columns = "min-content min-content"; - }; - card = { - padding = "4px"; - background = "var(--red)"; - border-radius = "24px"; - width = "90px"; - margin-right = "5px"; - }; - img_cell = { - justify-self = "start"; - width = "25px"; - height = "25px"; - background = "var(--contrast1)"; - border-radius = "100%"; - }; - name = { - font-size = "12px"; - color = "var(--black)"; - font-weight = 500; - justify-self = "start"; - align-self = "center"; - padding-left = "5px"; - }; - icon = { - width = "14px"; - color = "var(--red)"; - align-self = "center"; - }; - }; - }; - }; - }; - }; - - custom_card_mediaplayer_bottom = { - template = "setup"; - show_label = true; - name = '' - [[[ - if (states[ entity.entity_id ].attributes.active_child == "media_player.playstation_5") { - return states["sensor.ps5_343_activity"].attributes.players - } else if (states[ entity.entity_id ].attributes.active_child == "media_player.steam_jlnbln") { - return 'jlnbln' - } else if (states[ entity.entity_id ].attributes.active_child == "media_player.plex_bedroom") { - return states[ entity.entity_id ].attributes.media_series_title - } else { - if (states[ entity.entity_id ].attributes.media_artist != null ) { - return states[ entity.entity_id ].attributes.media_artist - } else { - return "Streaming" - } - } - ]]] - ''; - label = "[[[ return states[ entity.entity_id ].attributes.media_title ]]]"; - show_state = false; - show_entity_picture = true; - entity_picture = '' - [[[ - if (states[ entity.entity_id ].attributes.active_child == "media_player.playstation_5") { - return states["sensor.ps5_343_activity"].attributes.title_image - } else if (states[ entity.entity_id ].attributes.active_child == "media_player.apple_tv_4k_2") { - if (states[ entity.entity_id ].attributes.media_artist == "Rocket League Esports") { - return '/local/images/rlesports.jpg'; - } else { - return states["media_player.apple_tv_4k_2"].attributes.entity_picture - } - } else if (states[ entity.entity_id ].attributes.active_child == "media_player.steam_jlnbln") { - return states["sensor.steam_76561197981585794"].attributes.game_image_main - } else if (states[ entity.entity_id ].attributes.active_child == "media_player.plex_bedroom") { - return states["media_player.plex_bedroom"].attributes.entity_picture - } else if (states[ entity.entity_id ].attributes.active_child == "media_player.music") { - if (states["media_player.music"].attributes.active_child == "media_player.bedroom_nest_music_assistant") { - return states["media_player.bedroom_speakers"].attributes.entity_picture - } else { - return states[ entity.entity_id ].attributes.entity_picture - } - } else if (states[ entity.entity_id ].attributes.active_child == "media_player.audiobook") { - if (states["input_select.audiobook"].state == "Harry Potter") { - if (states["input_number.hp_book"].state == 1.0) { - return "/local/images/HP_1.jpg"; - } else if (states["input_number.hp_book"].state == 2.0) { - return "/local/images/HP_2.jpg"; - } else if (states["input_number.hp_book"].state == 3.0) { - return "/local/images/HP_3.jpg"; - } else if (states["input_number.hp_book"].state == 4.0) { - return "/local/images/HP_4.jpg"; - } else if (states["input_number.hp_book"].state == 5.0) { - return "/local/images/HP_5.jpg"; - } else if (states["input_number.hp_book"].state == 6.0) { - return "/local/images/HP_6.jpg"; - } else if (states["input_number.hp_book"].state == 7.0) { - return "/local/images/HP_7.jpg"; - } - } - } else { - return states[ entity.entity_id ].attributes.entity_picture - } - ]]] - ''; - icon = '' - [[[ - if (states[ entity.entity_id ].attributes.active_child == "media_player.playstation_5") { - return 'mdi:sony-playstation' - } else if (states[ entity.entity_id ].attributes.app_name == "YouTube") { - return 'mdi:youtube-tv' - } else if (states[ entity.entity_id ].attributes.app_name == "Netflix") { - return 'mdi:netflix' - } else if (states[ entity.entity_id ].attributes.app_name == "Plex") { - return 'mdi:plex' - } else { - return 'mdi:television-guide' - } - ]]] - ''; - styles = { - grid = { - grid-template-areas = ''"i play_state button" "i l button" "i n button" "bar bar bar"''; - grid-template-columns = "min-content"; - grid-template-rows = "min-content min-content min-content"; - column-gap = 15; - }; - icon = { - width = 60; - height = 60; - color = "var(--red)"; - }; - card = { - padding = "15px 15px 15px 15px"; - height = 180; - width = "100vw"; - overflow = "hidden"; - position = '' - [[[ - if (states["input_boolean.debug_rounded"].state == "on") { - return 'static' - } else { - return 'fixed' - } - ]]] - ''; - margin = 0; - bottom = 0; - left = 0; - z-index = 2; - border-radius = "20px 20px 0px 0px"; - box-shadow = "rgba(14, 30, 37, 0.12) 0px 2px 4px 0px, rgba(14, 30, 37, 0.32) 0px 2px 16px 0px"; - }; - img_cell = { - background = "none"; - border-radius = 10; - width = 60; - height = 60; - justify-self = "start"; - align-self = "start"; - left = 20; - }; - entity_picture.object-fit = "cover"; - state = { - font-size = 10; - justify-self = "start"; - align-self = "start"; - color = "var(--contrast10)"; - padding-right = 25; - }; - name = { - font-size = 11; - justify-self = "start"; - align-self = "start"; - padding-right = 20; - animation = "my-animation 15s linear infinite"; - }; - label = { - justify-self = "start"; - align-self = "end"; - font-weight = 700; - padding-right = 20; - font-size = 13; - margin-top = "-5px"; - }; - custom_fields = { - button = { - justify-self = "end"; - align-self = "center"; - padding-right = 20; - padding-top = 7; - }; - play_state = { - font-size = 10; - justify-self = "start"; - align-self = "start"; - color = "var(--contrast14)"; - padding-right = 21; - padding-top = 10; - }; - progress = { - background-color = "var(--contrast10)"; - position = "absolute"; - top = "unset"; - bottom = 90; - left = 40; - height = 2; - width = "80%"; - }; - bar = { - background-color = "var(--green)"; - position = "absolute"; - bottom = 90; - left = 40; - top = "unset"; - height = 2; - z-index = 1; - transition = "1s ease-out"; - }; - }; - }; - custom_fields = { - button.card = { - type = "custom:button-card"; - icon = "mdi:play"; - entity = "[[[ return entity.entity_id ]]]"; - show_name = false; - tap_action = { - action = "call-service"; - service = "media_player.media_play_pause"; - target.entity_id = "[[[ return entity.entity_id ]]]"; - }; - styles = { - card = { - overflow = "visible"; - background = "var(--contrast6)"; - "border-radius" = 10; - }; - icon = { - width = 17; - color = "var(--contrast16)"; - }; - img_cell = { - padding = 10; - width = 20; - }; - }; - state = [ - { - value = "playing"; - icon = '' - [[[ - if (states[entity.entity_id].attributes.active_child == "media_player.playstation_5") { - return 'mdi:sony-playstation' - } else if (states[entity.entity_id].attributes.active_child == "media_player.steam_jlnbln") { - return 'mdi:steam' - } else { - return 'mdi:pause' - } - ]]] - ''; - } - ]; - }; - play_state = '' - [[[ - if (states[entity.entity_id].attributes.active_child == "media_player.spotify_james") { - return states[entity.entity_id].state + " - " + states[entity.entity_id].attributes.source - } else if (states[entity.entity_id].attributes.active_child == "media_player.playstation_5") { - return states[entity.entity_id].state + " - Playstation 5" - } else if (states[entity.entity_id].attributes.active_child == "media_player.audiobook") { - return states[entity.entity_id].state + " - Alarm Clock" - } else if (states[entity.entity_id].attributes.active_child == "media_player.plex_bedroom") { - return states[entity.entity_id].state + " - Bedroom TV" - } else if (states[entity.entity_id].attributes.active_child == "media_player.steam_jlnbln") { - return states[entity.entity_id].state + " - Steam" - } else if (states[entity.entity_id].attributes.active_child == "media_player.apple_tv_4k_2") { - return states[entity.entity_id].state + " - " + states[entity.entity_id].attributes.app_name - } else { - return states[entity.entity_id].state + " - " + states["sensor.music_room"].state - } - ]]] - ''; - bar = '' - [[[ - if (entity.attributes.media_position !== undefined) { - setTimeout(() => { - let elt = this.shadowRoot, - card = elt.getElementById('card'), - container = elt.getElementById('container'), - bar = elt.getElementById('bar'); - if (elt && card && container && bar) { - card.insertBefore(bar, container); - function update() { - let mediaPositionUpdatedAt = entity.attributes.media_position_updated_at, - mediaPosition = entity.attributes.media_position, - mediaDuration = entity.attributes.media_duration, - mediaContentType = entity.attributes.media_content_type; - let percentage = entity.state === 'paused' - ? (mediaPosition / mediaDuration * 80) - : entity.state === 'playing' - ? (((Date.now() / 1000) - (new Date(mediaPositionUpdatedAt).getTime() / 1000) + mediaPosition) / mediaDuration * 80) - : 0; - bar.style.width = percentage.toFixed(1) + '%'; - requestAnimationFrame(update); - } - requestAnimationFrame(update); - } - }, 0); - return ' ';} - ]]] - ''; - }; - }; - - custom_card_timer_bottom = { - template = "setup"; - type = "custom:button-card"; - entity = '' - [[[ - if (states["sensor.kitchen_timer"].state == "on") { - return 'sensor.kitchen_timer' - } else if (states["sensor.living_room_timer"].state == "on") { - return 'sensor.living_room_timer' - } else if (states["sensor.office_timer"].state == "on") { - return 'sensor.office_timer' - } else if (states["sensor.bed_room_timer"].state == "on") { - return 'sensor.bed_room_timer' - } else if (states["sensor.bathroom_timer"].state == "on") { - return 'sensor.bathroom_timer' - } else { - return 'sensor.kitchen_timer' - } - ]]] - ''; - show_name = true; - show_icon = false; - show_label = false; - show_state = false; - tap_action.action = "more-info"; - hold_action.action = "more-info"; - custom_fields = { - bar = '' - [[[ - var color = "var(--green)"; - var state = 100 - states[ entity.entity_id ].attributes.remaining_perc; - if (state < 10) color = "var(--red)"; - else if (state < 50) color = "var(--yellow)"; - else if (state < 90) color = "var(--orange)"; - return ` -
-
-
-
` - ]]] - ''; - rem = { - card = { - type = "custom:button-card"; - entity = entity.id; - name = template.remainingTime; - show_icon = false; - styles = { - card = { - width = "min"; - background = "none"; - overflow = "visible"; - }; - name = { - font-size = 14; - margin-top = 6; - font-weight = 600; - color = "var(--contrast20)"; - }; - }; - }; - }; - icon1 = ''[[[ return ''; ]]]''; - }; - styles = { - grid = { - grid-template-areas = ''"rem icon1" "n icon2" "bar bar"''; - grid-template-rows = "min-content min-content min-content"; - grid-template-columns = "60% 40%"; - }; - card = { - padding = "15px 35px 15px 35px"; - background = "var(--contrast2)"; - height = template.stateIfElse { - entity = "media_player.music_and_tv"; - states = [ - "playing" - "paused" - ]; - ifTrue = "280px"; - ifFalse = "180px"; - }; - width = "100vw"; - overflow = "hidden"; - position = template.stateOnIfElse { - entity = "input_boolean.debug_rounded"; - valueOn = "static"; - valueOff = "fixed"; - }; - margin = 0; - bottom = 0; - left = 0; - z-index = 1; - border-radius = "20px 20px 0px 0px"; - box-shadow = "rgba(14, 30, 37, 0.12) 0px 2px 4px 0px, rgba(14, 30, 37, 0.32) 0px 2px 16px 0px"; - }; - img_cell = { - position = "absolute"; - top = "20%"; - left = "40%"; - overflow = "visible"; - }; - label = { - text-align = "left"; - font-size = 18; - font-weight = 500; - justify-self = "start"; - align-self = "start"; - overflow = "visible"; - color = "var(--contrast20)"; - }; - name = { - text-align = "left"; - font-size = 12; - justify-self = "start"; - align-self = "start"; - overflow = "visible"; - color = "var(--contrast20)"; - }; - custom_fields = { - bar = { - justify-self = "start"; - align-self = "start"; - margin-top = 10; - width = "100%"; - height = template.stateOnIfElse { - entity = "entity.state"; - valueOn = "12px"; - valueOff = "0px"; - }; - background = "var(--contrast1)"; - border-radius = 24; - }; - rem = { - justify-self = "start"; - font-size = 14; - font-weight = 600; - align-self = "end"; - height = template.stateOnIfElse { - entity = "entity.state"; - valueOn = "27px"; - valueOff = "0px"; - }; - }; - icon1 = { - justify-self = "end"; - align-self = "start"; - width = 24; - color = "var(--contrast20)"; - }; - }; - - hold_action.action = "more-info"; - }; - }; -} diff --git a/hosts/server/nixcloud/home-assistant/dashboard/views/energy.nix b/hosts/server/nixcloud/home-assistant/dashboard/views/energy.nix index e7e2cbcc7..3b2cfd575 100644 --- a/hosts/server/nixcloud/home-assistant/dashboard/views/energy.nix +++ b/hosts/server/nixcloud/home-assistant/dashboard/views/energy.nix @@ -6,7 +6,7 @@ let ''; }; - navbar = import ../components/navbar.nix { }; + navbar = import ../components/navbar.nix { inherit lib; }; in with (import ../lib.nix { inherit lib; }); mkView { @@ -51,8 +51,11 @@ mkView { icon = "mdi:flash"; }) { - type = "markdown"; - content = "Add power graphs (e.g., history-graph/statistics-graph) for your power sensors here."; + type = "history-graph"; + hours_to_show = 24; + entities = [ + { entity = "sensor.power_consumption"; } + ]; } ]; }) @@ -64,12 +67,10 @@ mkView { icon = "mdi:gauge"; }) { - type = "markdown"; - content = "Add your energy-related sensors (e.g., daily energy, grid import/export) to an Entities card here."; + type = "energy-date-selection"; } { - type = "entities"; - entities = [ ]; + type = "energy-solar-consumed-gauge"; } ]; }) diff --git a/hosts/server/nixcloud/home-assistant/dashboard/views/home.nix b/hosts/server/nixcloud/home-assistant/dashboard/views/home.nix index 3bda15c4b..4818f7fc6 100644 --- a/hosts/server/nixcloud/home-assistant/dashboard/views/home.nix +++ b/hosts/server/nixcloud/home-assistant/dashboard/views/home.nix @@ -1,8 +1,8 @@ { lib }: let viewHeader = import ../components/view-header.nix { }; - navbar = import ../components/navbar.nix { }; - adminPanel = import ../components/admin-panel.nix; + navbar = import ../components/navbar.nix { inherit lib; }; + adminPanel = import ../components/admin-panel.nix { inherit lib; }; in with (import ../lib.nix { inherit lib; }); mkView { @@ -24,5 +24,414 @@ mkView { rows = "auto"; }; }) + + # (mkGridSection) + + # Popups + (mkGridSection { + cards = [ + (mkVerticalStack { + cards = [ + (mkBubbleCard { + name = "Bedroom"; + icon = "mdi:bed-king-outline"; + cardType = "pop-up"; + hash = "#bedroom"; + }) + + (mkMushroomChipsCard { + chips = [ + (mkEntity { + entity = "binary_sensor.bedroom_presence_sensor"; + contentInfo = "last-changed"; + icon = "mdi:motion-sensor"; + iconColor = "yellow"; + }) + (mkEntity { + entity = "binary_sensor.bedroom_window_sensor"; + icon = "mdi:window-closed-variant"; + iconColor = "red"; + }) + (mkEntity { + entity = "binary_sensor.bedroom_door_sensor"; + icon = "mdi:door-closed"; + iconColor = "blue"; + }) + ]; + }) + + (mkHeading { + heading = "Climate"; + heading_style = "title"; + icon = "mdi:thermostat"; + badges = [ + (mkEntity { + entity = "climate.bedroom_ac"; + stateContent = "current_temperature"; + color = "red"; + showIcon = true; + showState = true; + tapAction = mkAction { action = "toggle"; }; + }) + (mkEntity { + entity = "climate.bedroom_ac"; + stateContent = "current_humidity"; + color = "blue"; + showIcon = true; + showState = true; + tapAction = mkAction { action = "toggle"; }; + }) + ]; + }) + (mkBubbleCard { + cardType = "climate"; + entity = "climate.bedroom_ac"; + showState = true; + subButton = [ + { + name = "HVAC modes menu"; + select_attribute = "hvac_modes"; + state_background = false; + show_arrow = false; + } + ]; + }) + + (mkHeading { + heading = "Devices"; + heading_style = "title"; + icon = "mdi:devices"; + badges = [ + (mkEntity { + entity = "input_boolean.bedroom_occupancy"; + icon = "mdi:light-switch"; + color = "red"; + showState = false; + showIcon = true; + tapAction = mkAction { action = "toggle"; }; + }) + (mkEntity { + entity = "input_boolean.bedroom_night_mode"; + showState = false; + showIcon = true; + color = "blue"; + tapAction = mkAction { action = "toggle"; }; + }) + ]; + }) + + (mkGridSection { + columns = 3; + cards = [ + (mkButtonCard { + entity = "binary_sensor.is_alarm_on"; + name = "Alarm"; + label = template.getState "sensor.wake_time_1"; + template = "nav_button_small"; + variables = { + navigationPath = "home#alarm"; + iconOn = "mdi:alarm"; + iconOff = "mdi:alarm-off"; + backgroundColorOn = jsColours.yellow; + backgroundColorOff = jsColours.contrast2; + colorOn = jsColours.black; + colorOff = jsColours.contrast20; + }; + }) + (mkConditional { + conditions = [ + (condState { + entity = "alarm_control_panel.security_system"; + state = "armed_night"; + }) + ]; + card = mkButtonCard { + entity = "input_boolean.waking_up"; + name = "Waking Up"; + label = template.getState "input_boolean.waking_up"; + showLabel = true; + tapAction = mkAction { + action = "toggle"; + haptic = "success"; + }; + template = "button_template"; + variables = { + icon2 = "mdi:weather-sunset-up"; + backgroundColorOff = jsColours.contrast2; + backgroundColorOn = jsColours.orange; + colorOn = jsColours.black; + colorOff = jsColours.contrast20; + }; + }; + }) + (mkConditional { + conditions = [ + (condState { + entity = "alarm_control_panel.security_system"; + state = "armed_night"; + }) + ]; + card = mkButtonCard { + entity = "input_boolean.going_to_bed"; + name = "Waking Up"; + label = template.getState "input_boolean.going_to_bed"; + showLabel = true; + tapAction = mkAction { + action = "toggle"; + haptic = "success"; + }; + template = "button_template"; + variables = { + icon2 = "mdi:bed"; + backgroundColorOff = jsColours.contrast2; + backgroundColorOn = jsColours.blue; + colorOn = jsColours.black; + colorOff = jsColours.contrast20; + }; + }; + }) + ]; + }) + + (mkGridSection { + columns = 1; + cards = [ + (mkHeading { + heading = "Lights"; + heading_style = "subtitle"; + icon = "mdi:lightbulb-group"; + badges = [ + (mkEntity { + entity = "light.bedroom_lights"; + showIcon = true; + showState = false; + color = "yellow"; + tapAction = mkAction { action = "toggle"; }; + }) + (mkEntity { + entity = "switch.adaptive_lighting_bedroom"; + showIcon = true; + showState = false; + color = "orange"; + tapAction = mkAction { action = "toggle"; }; + }) + (mkEntity { + entity = "switch.adaptive_lighting_adapt_color_bedroom"; + showIcon = true; + showState = false; + color = "pink"; + tapAction = mkAction { action = "toggle"; }; + }) + (mkEntity { + entity = "switch.adaptive_lighting_adapt_brightness_bedroom"; + showIcon = true; + showState = false; + color = "yellow"; + tapAction = mkAction { action = "toggle"; }; + }) + (mkEntity { + entity = "switch.adaptive_lighting_sleep_mode_bedroom"; + showIcon = true; + showState = false; + color = "blue"; + tapAction = mkAction { action = "toggle"; }; + }) + ]; + }) + + (mkGridSection { + columns = 1; + cards = [ + (mkButtonCard { + name = "Top Light"; + entity = "lighting.bedroom_light"; + template = "light_rgb"; + }) + ]; + }) + ]; + }) + + (mkHeading { + heading = "Media"; + heading_style = "title"; + icon = "mdi:music"; + }) + (mkSwipeCard { + cardWidth = "calc(100% - 48px)"; + parameters = { + centeredSlides = true; + slidesPerView = "auto"; + spaceBetween = 16; + initialSlide = 0; + }; + cards = [ + (mkConditional { + conditions = [ + (condState { + entity = "media_player.bedroom_speakers"; + state_not = "standby"; + }) + (condState { + entity = "media_player.bedroom_speakers"; + state_not = "off"; + }) + ]; + card = mkButtonCard { + entity = "media_player.bedroom_speakers"; + template = "custom_card_mediaplayer_music"; + }; + }) + + (mkConditional { + conditions = [ + (condState { + entity = "sensor.bedroom_next_timer"; + state_not = "unknown"; + }) + (condState { + entity = "sensor.bedroom_next_timer"; + state_not = "unavailable"; + }) + ]; + card = mkButtonCard { + entity = "sensor.bedroom_next_timer"; + name = "Bedroom Timer"; + icon = "mdi:timer-outline"; + showName = true; + showIcon = true; + showLabel = false; + showState = false; + tapAction = mkAction { + action = "more-info"; + }; + holdAction = mkAction { + action = "navigate"; + }; + customFields = { + bar = '' + [[[ + var color = "var(--green)"; + var state = 100 - states['sensor.bed_room_timer'].attributes.remaining_perc; + if (state < 10) color = "var(--red)"; + else if (state < 50) color = "var(--yellow)"; + else if (state < 90) color = "var(--orange)"; + return ` +
+
+
+
` + ]]] + ''; + rem.card = mkConditional { + conditions = [ + (condState { + entity = "sensor.bedroom_next_timer"; + state_not = "unknown"; + }) + ]; + card = mkButtonCard { + entity = "sensor.bedroom_next_timer"; + name = template.remainingTime; + showIcon = false; + styles = mkStyles { + card = { + width = "min"; + background = "none"; + overflow = "visible"; + }; + name = { + font-size = "14px"; + margin-top = "6px"; + font-weight = 600; + color = jsColours.contrast20; + }; + }; + }; + }; + icon1 = template.returnIcon "mdi:bed-king-outline"; + }; + styles = mkStyles { + grid = { + grid-template-areas = "\"rem icon1\" \"n icon2\" \"bar bar\""; + grid-template-rows = "24px 1fr 24px min-content min-content min-content"; + grid-template-columns = "60% 40%"; + }; + card = { + height = "100%"; + padding = "1rem"; + background = jsColours.contrast2; + }; + img_cell = { + position = "absolute"; + top = "20%"; + left = "40%"; + overflow = "visible"; + }; + icon = { + position = "absolute"; + width = "20em"; + opacity = "20%"; + color = jsColours.contrast20; + transform = "rotate(-20deg)"; + }; + label = { + text-align = "left"; + font-size = "18px"; + font-weight = 500; + justify-self = "start"; + align-self = "end"; + overflow = "visible"; + color = jsColours.contrast20; + }; + name = { + text-align = "left"; + font-size = "12px"; + justify-self = "start"; + align-self = "center"; + overflow = "visible"; + color = jsColours.contrast20; + }; + custom_fields = { + bar = { + justify-self = "start"; + width = "100%"; + height = template.stateIfElse { + entity = "entity.state"; + states = [ "unknown" ]; + ifTrue = "0px"; + ifFalse = "12px"; + }; + }; + rem = { + justify-self = "start"; + font-size = "14px"; + font-weight = 600; + align-self = "end"; + height = template.stateIfElse { + entity = "entity.state"; + states = [ "unknown" ]; + ifTrue = "0px"; + ifFalse = "27px"; + }; + }; + }; + icon1 = { + justify-self = "end"; + width = "24px"; + color = jsColours.contrast20; + }; + }; + }; + }) + ]; + }) + ]; + }) + ]; + grid_options = { + column_span = 3; + }; + }) ]; } diff --git a/hosts/server/nixcloud/home-assistant/dashboard/views/music.nix b/hosts/server/nixcloud/home-assistant/dashboard/views/music.nix index 90944be5c..308d87a5e 100644 --- a/hosts/server/nixcloud/home-assistant/dashboard/views/music.nix +++ b/hosts/server/nixcloud/home-assistant/dashboard/views/music.nix @@ -1,9 +1,10 @@ { lib, ... }: let viewHeader = import ../components/view-header.nix { }; - navbar = import ../components/navbar.nix { }; + navbar = import ../components/navbar.nix { inherit lib; }; + dashLib = import ../lib.nix { inherit lib; }; in -with (import ../lib.nix { inherit lib; }); +with dashLib; mkView { title = "Music"; header = viewHeader; @@ -47,285 +48,40 @@ mkView { }; }; custom_fields = { - item1.card = { - type = "custom:button-card"; - entity = "media_player.bed_room"; + item1.card = mkMusicRoomButton { + room = "bed_room"; icon = "mdi:bed-king-outline"; - show_label = false; - show_state = true; - show_entity_picture = true; - styles = { - card = [ - { border-radius = "0px"; } - { box-shadow = "none"; } - { padding-right = "5px"; } - { background = "none"; } - ]; - grid = [ - { grid-template-areas = ''" 'i n' 'i s' "''; } - { grid-template-columns = "min-content"; } - { column-gap = "10px"; } - ]; - entity_picture = [ { border-radius = "100%"; } ]; - icon = [ { width = 20; } ]; - img_cell = [ { width = 20; } ]; - name = [ - { justify-self = "start"; } - { font-size = 10; } - { font-weight = 500; } - { color = "var(--contrast14)"; } - ]; - state = [ - { justify-self = "start"; } - { font-size = 15; } - { font-weight = 700; } - ]; - }; - tap_action.action = "more-info"; - double_tap_action = { - action = "perform-action"; - perform_action = "script.play_on_repeat_on_alarm_clock"; - }; + script = "script.play_on_repeat_on_alarm_clock"; }; - item2.card = { - type = "custom:button-card"; - entity = "media_player.office"; + item2.card = mkMusicRoomButton { + room = "office"; icon = "mdi:monitor"; - show_label = false; - show_state = true; - show_entity_picture = true; - styles = { - card = [ - { border-radius = "0px"; } - { box-shadow = "none"; } - { padding-right = "5px"; } - { background = "none"; } - ]; - grid = [ - { grid-template-areas = ''" 'i n' 'i s' "''; } - { grid-template-columns = "min-content"; } - { column-gap = "10px"; } - ]; - entity_picture = [ { border-radius = "100%"; } ]; - icon = [ { width = 20; } ]; - img_cell = [ { width = 20; } ]; - name = [ - { justify-self = "start"; } - { font-size = 10; } - { font-weight = 500; } - { color = "var(--contrast14)"; } - ]; - state = [ - { justify-self = "start"; } - { font-size = 15; } - { font-weight = 700; } - ]; - }; - tap_action.action = "more-info"; - double_tap_action = { - action = "perform-action"; - perform_action = "script.play_on_repeat_on_office_nest"; - }; + script = "script.play_on_repeat_on_office_nest"; }; - item3.card = { - type = "custom:button-card"; - entity = "media_player.kitchen"; + item3.card = mkMusicRoomButton { + room = "kitchen"; icon = "mdi:silverware-variant"; - show_label = false; - show_state = true; - show_entity_picture = true; - styles = { - card = [ - { border-radius = "0px"; } - { box-shadow = "none"; } - { padding-right = "5px"; } - { background = "none"; } - ]; - grid = [ - { grid-template-areas = ''" 'i n' 'i s' "''; } - { grid-template-columns = "min-content"; } - { column-gap = "10px"; } - ]; - entity_picture = [ { border-radius = "100%"; } ]; - icon = [ { width = 20; } ]; - img_cell = [ { width = 20; } ]; - name = [ - { justify-self = "start"; } - { font-size = 10; } - { font-weight = 500; } - { color = "var(--contrast14)"; } - ]; - state = [ - { justify-self = "start"; } - { font-size = 15; } - { font-weight = 700; } - ]; - }; - tap_action.action = "more-info"; - double_tap_action = { - action = "perform-action"; - perform_action = "script.play_on_repeat_on_kitchen_nest"; - }; + script = "script.play_on_repeat_on_kitchen_nest"; }; - item4.card = { - type = "custom:button-card"; - entity = "media_player.bathroom"; + item4.card = mkMusicRoomButton { + room = "bathroom"; icon = "mdi:paper-roll-outline"; - show_label = false; - show_state = true; - show_entity_picture = true; - styles = { - card = [ - { border-radius = "0px"; } - { box-shadow = "none"; } - { padding-right = "5px"; } - { background = "none"; } - ]; - grid = [ - { grid-template-areas = ''" 'i n' 'i s' "''; } - { grid-template-columns = "min-content"; } - { column-gap = "10px"; } - ]; - entity_picture = [ { border-radius = "100%"; } ]; - icon = [ { width = 20; } ]; - img_cell = [ { width = 20; } ]; - name = [ - { justify-self = "start"; } - { font-size = 10; } - { font-weight = 500; } - { color = "var(--contrast14)"; } - ]; - state = [ - { justify-self = "start"; } - { font-size = 15; } - { font-weight = 700; } - ]; - }; - tap_action.action = "more-info"; - double_tap_action = { - action = "perform-action"; - perform_action = "script.play_on_repeat_on_bathroom_nest"; - }; + script = "script.play_on_repeat_on_bathroom_nest"; }; - item5.card = { - type = "custom:button-card"; - entity = "media_player.spare_room"; + item5.card = mkMusicRoomButton { + room = "spare_room"; icon = "mdi:desk"; - show_label = false; - show_state = true; - show_entity_picture = true; - styles = { - card = [ - { border-radius = "0px"; } - { box-shadow = "none"; } - { padding-right = "5px"; } - { background = "none"; } - ]; - grid = [ - { grid-template-areas = ''" 'i n' 'i s' "''; } - { grid-template-columns = "min-content"; } - { column-gap = "10px"; } - ]; - entity_picture = [ { border-radius = "100%"; } ]; - icon = [ { width = 20; } ]; - img_cell = [ { width = 20; } ]; - name = [ - { justify-self = "start"; } - { font-size = 10; } - { font-weight = 500; } - { color = "var(--contrast14)"; } - ]; - state = [ - { justify-self = "start"; } - { font-size = 15; } - { font-weight = 700; } - ]; - }; - tap_action.action = "more-info"; - double_tap_action = { - action = "perform-action"; - perform_action = "script.play_on_repeat_on_guest_room_nest"; - }; + script = "script.play_on_repeat_on_guest_room_nest"; }; - item6.card = { - type = "custom:button-card"; - entity = "media_player.everywhere"; + item6.card = mkMusicRoomButton { + room = "everywhere"; icon = "mdi:home-outline"; - show_label = false; - show_state = true; - show_entity_picture = true; - styles = { - card = [ - { border-radius = "0px"; } - { box-shadow = "none"; } - { padding-right = "5px"; } - { background = "none"; } - ]; - grid = [ - { grid-template-areas = ''" 'i n' 'i s' "''; } - { grid-template-columns = "min-content"; } - { column-gap = "10px"; } - ]; - entity_picture = [ { border-radius = "100%"; } ]; - icon = [ { width = 20; } ]; - img_cell = [ { width = 20; } ]; - name = [ - { justify-self = "start"; } - { font-size = 10; } - { font-weight = 500; } - { color = "var(--contrast14)"; } - ]; - state = [ - { justify-self = "start"; } - { font-size = 15; } - { font-weight = 700; } - ]; - }; - tap_action.action = "more-info"; - double_tap_action = { - action = "perform-action"; - perform_action = "script.play_on_repeat_on_nest_party"; - }; + script = "script.play_on_repeat_on_nest_party"; }; - item7.card = { - type = "custom:button-card"; - entity = "media_player.living_room"; + item7.card = mkMusicRoomButton { + room = "living_room"; icon = "mdi:sofa-outline"; - show_label = false; - show_state = true; - show_entity_picture = true; - styles = { - card = [ - { border-radius = "0px"; } - { box-shadow = "none"; } - { padding-right = "5px"; } - { background = "none"; } - ]; - grid = [ - { grid-template-areas = ''" 'i n' 'i s' "''; } - { grid-template-columns = "min-content"; } - { column-gap = "10px"; } - ]; - entity_picture = [ { border-radius = "100%"; } ]; - icon = [ { width = 20; } ]; - img_cell = [ { width = 20; } ]; - name = [ - { justify-self = "start"; } - { font-size = 10; } - { font-weight = 500; } - { color = "var(--contrast14)"; } - ]; - state = [ - { justify-self = "start"; } - { font-size = 15; } - { font-weight = 700; } - ]; - }; - tap_action.action = "more-info"; - double_tap_action = { - action = "perform-action"; - perform_action = "script.play_on_repeat_on_living_room_nest"; - }; + script = "script.play_on_repeat_on_living_room_nest"; }; }; } @@ -359,12 +115,12 @@ mkView { } { condition = "state"; - entity = "media_player.music"; + entity = mediaPlayers.special.music; state_not = "off"; } { condition = "state"; - entity = "media_player.music"; + entity = mediaPlayers.special.music; state_not = "standby"; } condition.mobileOnly @@ -374,273 +130,51 @@ mkView { cards = [ { square = true; - columns = 7; + columns = 6; type = "grid"; cards = [ - { - type = "custom:button-card"; - entity = "media_player.everywhere"; + (mkMusicRoomTransferButton { + room = "everywhere"; icon = "mdi:home-outline"; - show_name = false; - aspect_ratio = "1/1"; - state = [ - { - value = "playing"; - icon = "mdi:play-circle-outline"; - } - { - value = "paused"; - icon = "mdi:pause-circle-outline"; - } - ]; - tap_action = { - action = "call-service"; - service = "music_assistant.transfer_queue"; - data.source_player = '' - [[[ - if (states['sensor.music_room'].state == "Living Room") return "media_player.living_room"; - else if (states['sensor.music_room'].state == "Kitchen") return "media_player.kitchen"; - else if (states['sensor.music_room'].state == "Bathroom") return "media_player.bathroom_2"; - else if (states['sensor.music_room'].state == "Bedroom") return "media_player.bedroom_speaker"; - else if (states['sensor.music_room'].state == "Spare Room") return "media_player.spare_room"; - else if (states['sensor.music_room'].state == "Everywhere") return "media_player.everywhere"; - else return ""; - ]]] - ''; - target.entity_id = "media_player.everywhere"; - haptic = "success"; - }; - double_tap_action = { - action = "call-service"; - service = "script.play_on_repeat_on_nest_party"; - haptic = "success"; - }; - styles.card = [ - { border-radius = "12px"; } - { background-color = "var(--teal)"; } - ]; - styles.icon = [ { color = "var(--black)"; } ]; - } - { - type = "custom:button-card"; - entity = "media_player.living_room"; + color = "teal"; + targetEntity = mediaPlayers.rooms.everywhere; + script = "script.play_on_repeat_on_nest_party"; + }) + (mkMusicRoomTransferButton { + room = "living_room"; icon = "mdi:sofa-outline"; - show_name = false; - aspect_ratio = "1/1"; - state = [ - { - value = "playing"; - icon = "mdi:play-circle-outline"; - } - { - value = "paused"; - icon = "mdi:pause-circle-outline"; - } - ]; - tap_action = { - action = "call-service"; - service = "music_assistant.transfer_queue"; - data.source_player = '' - [[[ - if (states['sensor.music_room'].state == "Living Room") return "media_player.living_room"; - else if (states['sensor.music_room'].state == "Kitchen") return "media_player.kitchen"; - else if (states['sensor.music_room'].state == "Bathroom") return "media_player.bathroom_2"; - else if (states['sensor.music_room'].state == "Bedroom") return "media_player.bedroom_speaker"; - else if (states['sensor.music_room'].state == "Spare Room") return "media_player.spare_room"; - else if (states['sensor.music_room'].state == "Everywhere") return "media_player.everywhere"; - else return ""; - ]]] - ''; - target.entity_id = "media_player.living_room_nest_2"; - haptic = "success"; - }; - double_tap_action = { - action = "call-service"; - service = "script.play_on_repeat_on_living_room_nest"; - haptic = "success"; - }; - styles.card = [ - { border-radius = "12px"; } - { background-color = "var(--green)"; } - ]; - styles.icon = [ { color = "var(--black)"; } ]; - } - { - type = "custom:button-card"; - entity = "media_player.bed_room"; + color = "green"; + targetEntity = mediaPlayers.rooms.living_room; + script = "script.play_on_repeat_on_living_room_nest"; + }) + (mkMusicRoomTransferButton { + room = "bed_room"; icon = "mdi:bed"; - show_name = false; - aspect_ratio = "1/1"; - state = [ - { - value = "playing"; - icon = "mdi:play-circle-outline"; - } - { - value = "paused"; - icon = "mdi:pause-circle-outline"; - } - ]; - tap_action = { - action = "call-service"; - service = "music_assistant.transfer_queue"; - data.source_player = '' - [[[ - if (states['sensor.music_room'].state == "Living Room") return "media_player.living_room"; - else if (states['sensor.music_room'].state == "Kitchen") return "media_player.kitchen"; - else if (states['sensor.music_room'].state == "Bathroom") return "media_player.bathroom_2"; - else if (states['sensor.music_room'].state == "Bedroom") return "media_player.bedroom_speaker"; - else if (states['sensor.music_room'].state == "Spare Room") return "media_player.spare_room"; - else if (states['sensor.music_room'].state == "Everywhere") return "media_player.everywhere"; - else return ""; - ]]] - ''; - target.entity_id = "media_player.bedroom_speakers"; - haptic = "success"; - }; - double_tap_action = { - action = "call-service"; - service = "script.play_on_repeat_on_alarm_clock"; - haptic = "success"; - }; - styles.card = [ - { border-radius = "12px"; } - { background-color = "var(--blue)"; } - ]; - styles.icon = [ { color = "var(--black)"; } ]; - } - { - type = "custom:button-card"; - entity = "media_player.kitchen"; + color = "blue"; + targetEntity = mediaPlayers.rooms.bedroom_speakers; + script = "script.play_on_repeat_on_alarm_clock"; + }) + (mkMusicRoomTransferButton { + room = "kitchen"; icon = "mdi:silverware-variant"; - show_name = false; - aspect_ratio = "1/1"; - state = [ - { - value = "playing"; - icon = "mdi:play-circle-outline"; - } - { - value = "paused"; - icon = "mdi:pause-circle-outline"; - } - ]; - tap_action = { - action = "call-service"; - service = "music_assistant.transfer_queue"; - data.source_player = '' - [[[ - if (states['sensor.music_room'].state == "Living Room") return "media_player.living_room"; - else if (states['sensor.music_room'].state == "Kitchen") return "media_player.kitchen"; - else if (states['sensor.music_room'].state == "Bathroom") return "media_player.bathroom_2"; - else if (states['sensor.music_room'].state == "Bedroom") return "media_player.bedroom_speaker"; - else if (states['sensor.music_room'].state == "Spare Room") return "media_player.spare_room"; - else if (states['sensor.music_room'].state == "Everywhere") return "media_player.everywhere"; - else return ""; - ]]] - ''; - target.entity_id = "media_player.kitchen"; - haptic = "success"; - }; - double_tap_action = { - action = "call-service"; - service = "script.play_on_repeat_on_kitchen_nest"; - haptic = "success"; - }; - styles.card = [ - { border-radius = "12px"; } - { background-color = "var(--yellow)"; } - ]; - styles.icon = [ { color = "var(--black)"; } ]; - } - { - type = "custom:button-card"; - entity = "media_player.bathroom"; + color = "yellow"; + targetEntity = mediaPlayers.rooms.kitchen; + script = "script.play_on_repeat_on_kitchen_nest"; + }) + (mkMusicRoomTransferButton { + room = "bathroom"; icon = "mdi:paper-roll-outline"; - show_name = false; - aspect_ratio = "1/1"; - state = [ - { - value = "playing"; - icon = "mdi:play-circle-outline"; - } - { - value = "paused"; - icon = "mdi:pause-circle-outline"; - } - ]; - tap_action = { - action = "call-service"; - service = "music_assistant.transfer_queue"; - data.source_player = '' - [[[ - if (states['sensor.music_room'].state == "Living Room") return "media_player.living_room"; - else if (states['sensor.music_room'].state == "Kitchen") return "media_player.kitchen"; - else if (states['sensor.music_room'].state == "Bathroom") return "media_player.bathroom_2"; - else if (states['sensor.music_room'].state == "Bedroom") return "media_player.bedroom_speaker"; - else if (states['sensor.music_room'].state == "Spare Room") return "media_player.spare_room"; - else if (states['sensor.music_room'].state == "Everywhere") return "media_player.everywhere"; - else return ""; - ]]] - ''; - target.entity_id = "media_player.bathroom"; - haptic = "success"; - }; - double_tap_action = { - action = "call-service"; - service = "script.play_on_repeat_on_bathroom_nest"; - haptic = "success"; - }; - styles.card = [ - { border-radius = "12px"; } - { background-color = "var(--purple)"; } - ]; - styles.icon = [ { color = "var(--black)"; } ]; - } - { - type = "custom:button-card"; - entity = "media_player.spare_room"; + color = "purple"; + targetEntity = mediaPlayers.rooms.bathroom; + script = "script.play_on_repeat_on_bathroom_nest"; + }) + (mkMusicRoomTransferButton { + room = "spare_room"; icon = "mdi:desk"; - show_name = false; - aspect_ratio = "1/1"; - state = [ - { - value = "playing"; - icon = "mdi:play-circle-outline"; - } - { - value = "paused"; - icon = "mdi:pause-circle-outline"; - } - ]; - tap_action = { - action = "call-service"; - service = "music_assistant.transfer_queue"; - data.source_player = '' - [[[ - if (states['sensor.music_room'].state == "Living Room") return "media_player.living_room"; - else if (states['sensor.music_room'].state == "Kitchen") return "media_player.kitchen"; - else if (states['sensor.music_room'].state == "Bathroom") return "media_player.bathroom_2"; - else if (states['sensor.music_room'].state == "Bedroom") return "media_player.bedroom_speaker"; - else if (states['sensor.music_room'].state == "Spare Room") return "media_player.spare_room"; - else if (states['sensor.music_room'].state == "Everywhere") return "media_player.everywhere"; - else return ""; - ]]] - ''; - target.entity_id = "media_player.spare_room"; - haptic = "success"; - }; - double_tap_action = { - action = "call-service"; - service = "script.play_on_repeat_on_guest_room_nest"; - haptic = "success"; - }; - styles.card = [ - { border-radius = "12px"; } - { background-color = "var(--orange)"; } - ]; - styles.icon = [ { color = "var(--black)"; } ]; - } + color = "orange"; + targetEntity = mediaPlayers.rooms.spare_room; + script = "script.play_on_repeat_on_guest_room_nest"; + }) ]; } { @@ -655,219 +189,30 @@ mkView { cards = [ { type = "custom:button-card"; - entity = "media_player.music"; + entity = mediaPlayers.special.music; template = "media_player"; } - { - type = "conditional"; - conditions = [ - { - condition = "numeric_state"; - entity = "sensor.music_assistant_playing_devices"; - above = 1; - } - { - condition = "state"; - entity = "media_player.everywhere"; - state_not = "playing"; - } - { - condition = "state"; - entity = "sensor.music_room"; - state_not = "Living Room"; - } - { - condition = "or"; - conditions = [ - { - condition = "state"; - entity = "media_player.living_room"; - state = "paused"; - } - { - condition = "state"; - entity = "media_player.living_room"; - state = "playing"; - } - ]; - } - ]; - card = { - type = "custom:button-card"; - entity = "media_player.living_room"; - template = "media_player"; - variables.room = "Living Room"; - variables.icon_1 = ""; - }; - } - { - type = "conditional"; - conditions = [ - { - condition = "numeric_state"; - entity = "sensor.music_assistant_playing_devices"; - above = 1; - } - { - condition = "state"; - entity = "media_player.everywhere"; - state_not = "playing"; - } - { - condition = "state"; - entity = "sensor.music_room"; - state_not = "Kitchen"; - } - { - condition = "or"; - conditions = [ - { - condition = "state"; - entity = "media_player.kitchen"; - state = "paused"; - } - { - condition = "state"; - entity = "media_player.kitchen"; - state = "playing"; - } - ]; - } - ]; - card = { - type = "custom:button-card"; - entity = "media_player.kitchen"; - template = "media_player"; - variables.room = "Kitchen"; - variables.icon_1 = ""; - }; - } - { - type = "conditional"; - conditions = [ - { - condition = "numeric_state"; - entity = "sensor.music_assistant_playing_devices"; - above = 1; - } - { - condition = "state"; - entity = "media_player.everywhere"; - state_not = "playing"; - } - { - condition = "state"; - entity = "sensor.music_room"; - state_not = "Bathroom"; - } - { - condition = "or"; - conditions = [ - { - condition = "state"; - entity = "media_player.bathroom"; - state = "paused"; - } - { - condition = "state"; - entity = "media_player.bathroom"; - state = "playing"; - } - ]; - } - ]; - card = { - type = "custom:button-card"; - entity = "media_player.bathroom"; - template = "media_player"; - variables.room = "Bathroom"; - variables.icon_1 = ""; - }; - } - { - type = "conditional"; - conditions = [ - { - condition = "numeric_state"; - entity = "sensor.music_assistant_playing_devices"; - above = 1; - } - { - condition = "state"; - entity = "media_player.everywhere"; - state_not = "playing"; - } - { - condition = "state"; - entity = "sensor.music_room"; - state_not = "Bedroom"; - } - { - condition = "or"; - conditions = [ - { - condition = "state"; - entity = "media_player.bedroom_nest_music_assistant"; - state = "paused"; - } - { - condition = "state"; - entity = "media_player.bedroom_nest_music_assistant"; - state = "playing"; - } - ]; - } - ]; - card = { - type = "custom:button-card"; - entity = "media_player.bedroom_speakers"; - template = "media_player"; - variables.room = "Bedroom"; - variables.icon_1 = ""; - }; - } - { - type = "conditional"; - conditions = [ - { - condition = "numeric_state"; - entity = "sensor.music_assistant_playing_devices"; - above = 1; - } - { - condition = "state"; - entity = "media_player.everywhere"; - state_not = "playing"; - } - { - condition = "state"; - entity = "sensor.music_room"; - state_not = "Spare Room"; - } - { - condition = "or"; - conditions = [ - { - condition = "state"; - entity = "media_player.spare_room"; - state = "paused"; - } - { - condition = "state"; - entity = "media_player.spare_room"; - state = "playing"; - } - ]; - } - ]; - card = { - type = "custom:button-card"; - entity = "media_player.spare_room"; - template = "media_player"; - variables.room = "Spare Room"; - variables.icon_1 = ""; - }; - } + (mkMusicSwipeCard { + room = "Living Room"; + entity = mediaPlayers.rooms.living_room; + }) + (mkMusicSwipeCard { + room = "Kitchen"; + entity = mediaPlayers.rooms.kitchen; + }) + (mkMusicSwipeCard { + room = "Bathroom"; + entity = mediaPlayers.rooms.bathroom; + }) + (mkMusicSwipeCard { + room = "Bedroom"; + entity = mediaPlayers.rooms.bedroom_speakers; + conditionEntity = mediaPlayers.rooms.bedroom_nest; + }) + (mkMusicSwipeCard { + room = "Spare Room"; + entity = mediaPlayers.rooms.spare_room; + }) ]; } ]; diff --git a/hosts/server/nixcloud/home-assistant/dashboard/views/security.nix b/hosts/server/nixcloud/home-assistant/dashboard/views/security.nix index b195e4986..35546833c 100644 --- a/hosts/server/nixcloud/home-assistant/dashboard/views/security.nix +++ b/hosts/server/nixcloud/home-assistant/dashboard/views/security.nix @@ -7,7 +7,7 @@ let ''; }; - navbar = import ../components/navbar.nix { }; + navbar = import ../components/navbar.nix { inherit lib; }; in mkView { title = "Security"; diff --git a/hosts/server/nixcloud/home-assistant/dashboard/views/server.nix b/hosts/server/nixcloud/home-assistant/dashboard/views/server.nix index bdabab6c6..a5d204c9e 100644 --- a/hosts/server/nixcloud/home-assistant/dashboard/views/server.nix +++ b/hosts/server/nixcloud/home-assistant/dashboard/views/server.nix @@ -1,7 +1,7 @@ { lib, ... }: with (import ../lib.nix { inherit lib; }); let - navbar = import ../components/navbar.nix { }; + navbar = import ../components/navbar.nix { inherit lib; }; viewHeader = import ../components/view-header.nix { content = '' # Server diff --git a/hosts/server/nixcloud/home-assistant/default.nix b/hosts/server/nixcloud/home-assistant/default.nix index cd16936c5..5c9e85e7d 100644 --- a/hosts/server/nixcloud/home-assistant/default.nix +++ b/hosts/server/nixcloud/home-assistant/default.nix @@ -43,7 +43,7 @@ pkgs.pyarlo spotifyaio - aiobotocore # For S3 Backup to Minio + # aiobotocore # For S3 Backup to Minio # Required for home-generative-agent integration # Source: https://github.com/goruck/home-generative-agent/blob/main/custom_components/home_generative_agent/manifest.json @@ -57,7 +57,7 @@ langchain-core langchain-ollama langchain-openai - langchain-google-genai + # langchain-google-genai ]; }; customComponents = with pkgs.home-assistant-custom-components; [ diff --git a/hosts/server/nixcloud/home-assistant/music.nix b/hosts/server/nixcloud/home-assistant/music.nix index 5f3642419..81eed6f00 100644 --- a/hosts/server/nixcloud/home-assistant/music.nix +++ b/hosts/server/nixcloud/home-assistant/music.nix @@ -10,10 +10,7 @@ _: { "chromecast" "hass" "hass_players" - "jellyfin" - "spotify" - "spotify_connect" - "ytmusic" + "opensubsonic" ]; }; }; diff --git a/hosts/server/nixcloud/home-assistant/static.nix b/hosts/server/nixcloud/home-assistant/static.nix new file mode 100644 index 000000000..4699a1e56 --- /dev/null +++ b/hosts/server/nixcloud/home-assistant/static.nix @@ -0,0 +1,24 @@ +# Helper for defining static elements for the HassIO configuration. +# Entities, Users, Rooms etc. +{ + entities = { + + }; + + # Each user needs a profile created in Home Assistant first. + users = { + james = { + displayName = "James"; + id = "3eea636aa3de4c7f9c662ad29c6e92e0"; + }; + + savannah = { + displayName = "Savannah"; + id = "82def695e9504f63b1eb09150073737d"; + }; + }; + + rooms = { + + }; +}