A Custom View Assist Dashboard for an Amazon Echo Show 5

View Assist dashboard running on an Amazon Echo Show 5 Gen 2

After watching this this dashboard video. I wanted to set up a similar bedside dashboard, with an always-on Home Assistant display. View_assist looks quiet nice as a bedside dashboard, but its primarily designed to be voice activate. I wanted something would be mostly touch activated with a side menu.

A quick warning: I am new to View Assist and this is an experimental personal setup. Back up your existing View Assist dashboard before replacing anything. Copy the complete contents of the Raw configuration editor and save them somewhere safe so you can restore the dashboard if necessary.

Prepare the Echo Show 5

An Amazon Echo Show does not provide a normal way to run a custom Home Assistant dashboard. The device must first be jailbroken and prepared to run View Assist. I have not repeated those instructions here because Mark Watt Tech already provides really nice detailed walkthroughs.

Follow the applicable tutorial carefully and confirm that the standard View Assist dashboard works on the Echo before installing this customized dashboard.

Install the dashboard

Before importing it:

  1. Install and configure View Assist.
  2. Confirm that the standard View Assist dashboard loads on the Echo.
  3. Install View Assist’s required Lovelace dependencies through HACS, including Button Card, Layout Card, and Card Mod.
  4. Installed the custom view-assist-menu-card
  5. Modify the View Assist dashboard
  6. Create the Lovelace dashboard

Installed the custom view-assist-menu-card

We need to create a small custom Lovelace card. This card can be used on both the View assist dashboard and the custom lovelace dashboards.

  • Reads menu_items from the active View Assist device.
  • Creates an always-visible menu on the right side of every View Assist view.
  • reduce the width of the standard screens so they are not behind the menu.
  • Shows light icons in yellow while the light is on.

Save the below file to you Home Assistance:

  • save as: : /local/view-assist-menu-card.js

Register it through the Home Assistant UI:

  • Open Settings → Dashboards.
  • Select the three-dot menu in the upper-right.
  • Select Resources.
  • Select Add resource.
  • Enter: /local/view-assist-menu-card.js?v=1
  • Select JavaScript module as the resource type.
  • Select Create.

Then perform a hard refresh on the browser:

  • Desktop: Ctrl+F5
  • Echo/browser display: reload the page or restart its browser app

You can how use the *view-assist-menu-card where its needed.

Modify the View Assist dashboard

Do not edit .storage/lovelace.view_assist directly. View Assist uses a Home Assistant storage-mode dashboard, so dashboard changes should be saved through the Raw configuration editor.

  • Open the View Assist dashboard in Home Assistant.
  • Select Edit dashboard from the three-dot menu.
  • Open the three-dot menu again and select Raw configuration editor.
  • Copy the current configuration into a backup file.
  • Replace the editor contents with the contents of the yaml below and save.
  • Refresh or restart the browser on the Echo Show.

Create the Lovelace dashboard

  • Create a new Lovelace dashboard
  • Add the view-assist-menu-card the dashboard

Configure the side menu

The side menu reads its entries from the View Assist entity associated with the device displaying the dashboard. The modified View Assist dashboard and the sample room Lovelace dashboard both use this same menu configuration. This allows the kitchen, study, bedroom, or other View Assist devices to have different menus without maintaining the menu entries separately on each dashboard.

Configure a device from:

Settings → Devices & services → View Assist → your device → Dashboard options → Display settings → Menu Items

Enter the items in the order you want them displayed. The first configured item appears at the top of the vertical menu.

view:weather|weather-partly-cloudy

The first value is the destination and the value after | is the Material Design icon name. Do not include the mdi: prefix.

Open another Home Assistant dashboard

view:/dashboard-myroom/0|view-dashboard

Lovelace dashboard for an Amazon Echo Show 5 Gen 2
For this, I create a small dashboard for each room that has an Echo. The dashboard provides access to the smart devices in that room, such as the air conditioner, lights, and fan. A destination beginning with / is treated as an absolute Home Assistant path. This is not as smooth as navigating between View Assist views. Opening a separate Lovelace dashboard reloads the whole page, and returning to View Assist reloads it again.

The sample room dashboard below includes a side-menu card that reads menu_items from the active View Assist device. Add this card to each separate Lovelace dashboard where you want the shared menu to appear. The card configuration stays the same; changes made under the device’s Menu Items setting are then reflected in both the View Assist dashboard and the room dashboard.

Add a light button

entity:light.study|lightbulb

Tapping the button toggles the light, while holding it opens the entity’s more-info dialog. The light icon is yellow when the light is on and returns to its normal white colour when it is off.

Two different icons can also be specified for the on and off states:

entity:light.study|lightbulb-on,lightbulb-off

Run a script or service

service:script.good_night|sleep

Tapping this button calls the configured Home Assistant service.

Predefined View Assist menu templates such as home, weather, camera, and music can also be used.

An example menu might look like this:

home
view:weather|weather-partly-cloudy
view:/dashboard-study/0|view-dashboard
entity:light.study|lightbulb
service:script.good_night|sleep

Final notes

This is a hack built on top of another hack, but it has turned the Echo Show 5 into a useful little Home Assistant control panel. The menu remains native to each View Assist device’s configuration, so I can change its buttons in one place and use them on both the View Assist and room Lovelace dashboards.

Keep a backup of the working dashboard, especially before updating View Assist or experimenting with further layout changes. If something breaks, restore the saved configuration through the View Assist dashboard’s Raw configuration editor.

Code

view-assist-menu-card

View Assist Menu Card
class ViewAssistMenuCard extends HTMLElement {
  static getConfigElement() {
    return document.createElement("hui-error-card");
  }

  static getStubConfig() {
    return {
      position: "right",
      reverse: true,
    };
  }

  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this._config = {};
    this._hass = undefined;
    this._signature = "";
  }

  setConfig(config) {
    this._config = {
      position: "right",
      reverse: true,
      width: "clamp(56px, 7vw, 90px)",
      padding: "0.5vw",
      gap: "0px",
      ...config,
    };
    this._signature = "";
    this._render();
  }

  set hass(hass) {
    this._hass = hass;
    this._render();
  }

  getCardSize() {
    return 1;
  }

  _getViewAssistEntity() {
    return (
      this._config.entity || localStorage.getItem("view_assist_sensor") || ""
    );
  }

  _getSignature(entityId, menuItems) {
    const entityStates = menuItems
      .filter((item) => typeof item === "string" && item.startsWith("entity:"))
      .map((item) => {
        const target = item.split("|")[0].slice(7);
        return `${target}:${this._hass?.states[target]?.state || ""}`;
      });

    return JSON.stringify({
      entityId,
      menuItems,
      entityStates,
      path: window.location.pathname,
      config: this._config,
    });
  }

  _normalisePath(path) {
    return path.replace(/\/{2,}/g, "/");
  }

  _getMenuItems(entityId) {
    const items = this._hass?.states[entityId]?.attributes?.menu_items;
    if (!Array.isArray(items)) {
      return [];
    }

    const filtered = items.filter(
      (item) => typeof item === "string" && item !== "menu",
    );
    return this._config.reverse ? [...filtered].reverse() : filtered;
  }

  _parseItem(entityId, item) {
    const entity = this._hass.states[entityId];
    const dashboard = (entity?.attributes?.dashboard || "/view-assist").replace(
      /\/$/,
      "",
    );
    const predefined = {
      home: {
        type: "view",
        target: entity?.attributes?.home_screen || `${dashboard}/clock`,
        icon: "home",
      },
      weather: {
        type: "view",
        target: `${dashboard}/weather`,
        icon: "weather-sunny",
      },
      camera: {
        type: "view",
        target: `${dashboard}/camera`,
        icon: "cctv",
      },
      music: {
        type: "view",
        target: `${dashboard}/music`,
        icon: "music",
      },
    };

    if (!item.includes(":")) {
      return predefined[item] || {
        type: "invalid",
        target: item,
        icon: "alert-circle-outline",
      };
    }

    const [definition, iconDefinition = "help-circle"] = item.split("|");
    const separator = definition.indexOf(":");
    const type = definition.slice(0, separator);
    let target = definition.slice(separator + 1);
    const icons = iconDefinition.split(",").map((icon) => icon.trim());

    if (type === "view" && !target.startsWith("/")) {
      target = `${dashboard}/${target}`;
    }

    return {
      type,
      target: type === "view" ? this._normalisePath(target) : target,
      icon: icons[0] || "help-circle",
      offIcon: icons[1],
    };
  }

  _createButton(entityId, item) {
    const parsed = this._parseItem(entityId, item);
    const button = document.createElement("button");
    button.type = "button";
    button.className = "menu-button";
    button.title = item;

    let iconName = parsed.icon;
    if (parsed.type === "entity") {
      const state = this._hass.states[parsed.target]?.state;
      if (state === "off" && parsed.offIcon) {
        iconName = parsed.offIcon;
      }
      button.classList.toggle("on", state === "on");
    }

    if (
      parsed.type === "view" &&
      window.location.pathname === parsed.target
    ) {
      button.classList.add("selected");
    }

    const icon = document.createElement("ha-icon");
    icon.setAttribute("icon", `mdi:${iconName}`);
    button.append(icon);

    button.addEventListener("click", () => {
      this._handleTap(entityId, parsed);
    });

    if (parsed.type === "entity") {
      button.addEventListener("contextmenu", (event) => {
        event.preventDefault();
        const moreInfo = new Event("hass-more-info", {
          bubbles: true,
          composed: true,
        });
        moreInfo.detail = { entityId: parsed.target };
        this.dispatchEvent(moreInfo);
      });
    }

    return button;
  }

  _handleTap(entityId, item) {
    if (!this._hass) {
      return;
    }

    if (item.type === "view") {
      this._hass.callService("view_assist", "navigate", {
        device: entityId,
        path: item.target,
      });
      return;
    }

    if (item.type === "entity") {
      const domain = item.target.split(".")[0];
      this._hass.callService(domain, "toggle", {
        entity_id: item.target,
      });
      return;
    }

    if (item.type === "service") {
      const separator = item.target.indexOf(".");
      if (separator > 0) {
        this._hass.callService(
          item.target.slice(0, separator),
          item.target.slice(separator + 1),
          {},
        );
      }
    }
  }

  _render() {
    if (!this.shadowRoot || !this._hass) {
      return;
    }

    const entityId = this._getViewAssistEntity();
    const menuItems = this._getMenuItems(entityId);
    const signature = this._getSignature(entityId, menuItems);
    if (signature === this._signature) {
      return;
    }
    this._signature = signature;

    this.shadowRoot.replaceChildren();

    const style = document.createElement("style");
    const position = this._config.position === "left" ? "left" : "right";
    style.textContent = `
      :host {
        display: block;
      }

      ha-card {
        position: fixed;
        ${position}: ${this._config.offset || "1.5vw"};
        top: 50%;
        transform: translateY(-50%);
        z-index: ${this._config.z_index || 10};
        width: ${this._config.width};
        padding: ${this._config.padding};
        box-sizing: border-box;
        border: none;
        border-radius: ${this._config.border_radius || "18px"};
        background: ${
          this._config.background || "rgba(20, 20, 20, 0.92)"
        };
        box-shadow: ${
          this._config.box_shadow || "0 4px 18px rgba(0, 0, 0, 0.35)"
        };
      }

      .menu {
        display: grid;
        grid-template-columns: 1fr;
        gap: ${this._config.gap};
      }

      .menu-button {
        display: grid;
        place-items: center;
        width: 100%;
        aspect-ratio: 1 / 1;
        margin: 0;
        padding: 0;
        border: none;
        border-radius: var(--ha-card-border-radius, 12px);
        color: white;
        background: transparent;
        cursor: pointer;
      }

      .menu-button ha-icon {
        width: 90%;
        height: 90%;
        --mdc-icon-size: 90%;
      }

      .menu-button.on {
        color: var(--warning-color);
      }

      .menu-button.selected {
        color: var(--primary-color);
      }

      .menu-button:active {
        background: rgba(255, 255, 255, 0.12);
      }
    `;

    const card = document.createElement("ha-card");
    const menu = document.createElement("div");
    menu.className = "menu";

    for (const item of menuItems) {
      menu.append(this._createButton(entityId, item));
    }

    card.append(menu);
    this.shadowRoot.append(style, card);
  }
}

if (!customElements.get("view-assist-menu-card")) {
  customElements.define("view-assist-menu-card", ViewAssistMenuCard);
}

window.customCards = window.customCards || [];
window.customCards.push({
  type: "view-assist-menu-card",
  name: "View Assist Menu Card",
  description: "Reusable menu sourced from a View Assist device configuration.",
});

Modified view_assist dashboard

This is a the modified version of the view assist dashboard with a side menu added.

Because the menu floats over the dashboard, the Clock home screen and Weather view also reserve a responsive gutter on the right. Their content stops before the menu, while their background image or colour still covers the full screen.

View Assist Dashboard with side menu
button_card_templates:
  variable_template:
    variables:
      dashboardversion: 1.3.2
      var_assistsat_entity: |-
        [[[
          return localStorage.getItem("view_assist_sensor")
        ]]]
      var_dashboard: |-
        [[[
          try {
            return hass.states[variables.var_assistsat_entity].attributes.dashboard || '/view-assist';
          } catch {
            return '/view-assist';
          }
        ]]]
      var_mic_switch: |-
        [[[
          try {
            var micdevice = hass.states[variables.var_assistsat_entity].attributes.mute_switch;
            return `${micdevice}`;
          } catch { return ""}
        ]]]
      var_wake_switch: |-
        [[[
          try {
            var micdevice = hass.states[variables.var_assistsat_entity].attributes.mic_device;
            if (micdevice.startsWith("assist_satellite.")) {
              var wakebutton = micdevice.replace("assist_satellite.", "button.") + "_wake";
              if (wakebutton in hass.states) {
                return `${wakebutton}`;
              }
            }
            return "";
          } catch {
            return "";
          }
        ]]]
      var_mediaplayer_device: |-
        [[[
          try {
            var mediadevice = hass.states[variables.var_assistsat_entity].attributes.mediaplayer_device;
            return `${mediadevice}`;
          } catch { return "";}
        ]]]
      var_mediaplayer_mute: |-
        [[[
          try {
            var mediadevice = hass.states[variables.var_assistsat_entity].attributes.mediaplayer_device;
            var mediaplayerstate = hass.states[mediadevice].attributes.is_volume_muted;
            return `${mediaplayerstate}`;
          } catch { return "";}
        ]]]
      var_assistsat_time_format: |-
        [[[
          if (variables.var_assistsat_entity_use_24_hour_time) {
            return '%H:%M';
          } else {
            return '%l:%M';
          }
        ]]]
      var_current_time: |-
        [[[
          return `<viewassist-clock server_time=true format='${variables.var_assistsat_time_format}'></viewassist-clock>`;
        ]]]
      var_date_short: |-
        [[[
          return `<viewassist-clock server_time=true format='%a, %b %e'></viewassist-clock>`;
        ]]]
      var_date_long: |-
        [[[
          return `<viewassist-clock server_time=true format='%A, %B %d, %Y'></viewassist-clock>`;
        ]]]
      var_assistsat_entity_font_style: |
        [[[
          try
          {
            return states[variables.var_assistsat_entity].attributes.font_style;
          } catch { return  "Roboto"}
        ]]]
      var_assistsat_entity_weather_entity: |
        [[[
          try
          {
            return states[variables.var_assistsat_entity].attributes.weather_entity;
          } catch { return  ""}
        ]]]
      var_assistsat_entity_responsive_status_icons_size: |
        [[[
          try {
            const baseSize = states[variables.var_assistsat_entity].attributes.status_icons_size || "6vw";

            if (window.viewAssistResponsive?.orientation === "portrait") {
              const numericValue = parseFloat(baseSize);
              const unit = baseSize.replace(numericValue.toString(), '');
              return `${numericValue * 1.5}${unit}`;
            }

            return baseSize;
          } catch {
            return window.viewAssistResponsive?.orientation === "portrait" ? "9vw" : "6vw";
          }
        ]]]
      var_assistsat_entity_use_24_hour_time: |
        [[[
          try
          {
            return states[variables.var_assistsat_entity].attributes.use_24_hour_time;
          } catch { return  false}
        ]]]
      var_title: |-
        [[[
          try
          {
            return states[variables.var_assistsat_entity].attributes.title;
          } catch { return  ""}
        ]]]
      var_message: |-
        [[[
          try
          {
            return states[variables.var_assistsat_entity].attributes.message;
          } catch { return  ""}
        ]]]
      var_image: |-
        [[[
          try
          {
            return states[variables.var_assistsat_entity].attributes.image;
          } catch { return  ""}
        ]]]
      var_font_size: |-
        [[[
          try
          {
            return states[variables.var_assistsat_entity].attributes.message_font_size;
          }
          catch { return  ""}
        ]]]
      var_weather_temperature: |-
        [[[
          try
          {
            return (states[variables.var_assistsat_entity_weather_entity].attributes.temperature  + '°');
          }
          catch { return  ""}
        ]]]
      var_weather_icon: |-
        [[[
          const weatherIconsDay  = {
            "clear-night": "mdi:weather-night",
            "cloudy": "mdi:weather-cloudy",
            "dust": "mdi:weather-dust",
            "fog": "mdi:weather-fog",
            "hail": "mdi:weather-hail",
            "hazy": "mdi:weather-hazy",
            "hurricane": "mdi:weather-hurricane",
            "lightning": "mdi:weather-lightning",
            "lightning-rainy": "mdi:weather-lightning-rainy",
            "partlycloudy": "mdi:weather-partly-cloudy",
            "partly-lightning": "mdi:weather-partly-lightning",
            "partly-rainy": "mdi:weather-partly-rainy",
            "partly-snowy": "mdi:weather-partly-snowy",
            "partly-snowy-rainy": "mdi:weather-partly-snowy-rainy",
            "pouring": "mdi:weather-pouring",
            "rainy": "mdi:weather-rainy",
            "snowy": "mdi:weather-snowy",
            "snowy-heavy": "mdi:weather-snowy-heavy",
            "snowy-rainy": "mdi:weather-snowy-rainy",
            "sunny": "mdi:weather-sunny",
            "tornado": "mdi:weather-tornado",
            "windy":  "mdi:weather-windy",
            "windy-variant": "mdi:weather-windy-variant",
          };
          const weatherIconsNight = {
            ...weatherIconsDay,
            clear: "mdi:weather-night",
            sunny: "mdi:weather-night",
            partlycloudy: "mdi:weather-night-partly-cloudy",
          };
          try
          {
            var condition = states[variables.var_assistsat_entity_weather_entity].state;
            if (states['sun.sun'] === 'above_horizon') {
              var weather_icon = weatherIconsDay[condition];
            } else {
              var weather_icon = weatherIconsDay[condition];
            }

            if (typeof(weather_icon) === 'undefined') {
              var weather_icon = "mdi:help"
            }
            return `${weather_icon}`;
          } catch { return  ""}
        ]]]
      var_current_view: |-
        [[[
          try {
            const pathname = window.location.pathname;
            const match = pathname.match(/\/view-assist\/([^\/]+)/);
            return match && match[1] ? match[1] : "";
          } catch {
            return "";
          }
        ]]]
  responsive_base:
    variables:
      var_orientation_sensor: |-
        [[[
          try {
            const orientationSensor = hass.states[variables.var_assistsat_entity]?.attributes?.orientation_sensor;

            if (orientationSensor && orientationSensor in hass.states) {
              const state = hass.states[orientationSensor].state;
              if (state === "portrait" || state === "landscape") {
                return state;
              }
            }

            return "";
          } catch {
            return "";
          }
        ]]]
      var_orientation:
        value: |-
          [[[
            const sensorOrientation = variables.var_orientation_sensor;
            const windowOrientation = window.innerHeight > window.innerWidth
              ? "portrait"
              : "landscape";
            const currentOrientation = sensorOrientation || windowOrientation;

            if (!window.viewAssistResponsive) {
              window.viewAssistResponsive = {
                orientation: currentOrientation,
                sensor_available: !!sensorOrientation,
                lastUpdate: Date.now()
              };
            } else {
              const oldOrientation = window.viewAssistResponsive.orientation;
              window.viewAssistResponsive.sensor_available = !!sensorOrientation;

              if (oldOrientation !== currentOrientation) {
                window.viewAssistResponsive.orientation = currentOrientation;
                window.viewAssistResponsive.lastUpdate = Date.now();

                window.dispatchEvent(new CustomEvent('view-assist-responsive-change', {
                  detail: window.viewAssistResponsive,
                  bubbles: true
                }));
              }
            }

            return window.viewAssistResponsive.orientation;
          ]]]
        force_eval: true
  body_template:
    template:
      - variable_template
      - status_icons_content
      - responsive_base
      - navbar_overlay
    show_state: false
    show_icon: false
    show_name: false
    tap_action:
      action: call-service
      service: view_assist.set_state
      service_data:
        entity_id: >-
          [[[ try { return variables.var_assistsat_entity } catch { return  ""
          }]]]
        mode: hold
    double_tap_action:
      action: call-service
      service: view_assist.set_state
      service_data:
        entity_id: >-
          [[[ try { return variables.var_assistsat_entity } catch { return  "" }
          ]]]
        mode: normal
    hold_action:
      action: call-service
      service: switch.toggle
      service_data:
        entity_id: '[[[ try { return variables.var_mic_switch } catch { return  "" } ]]]'
    styles:
      grid:
        - grid-template-areas: |
            "title status"
            "message message"
            "assist assist"
        - grid-template-rows: min-content 1fr
        - grid-template-columns: 1fr 1fr
        - row-gap: .5rem
      card:
        - min-height: |-
            [[[
              try {
                const screenMode = hass.states[variables.var_assistsat_entity]?.attributes?.screen_mode;
                if (screenMode === 'no_hide' || screenMode === 'hide_sidebar') {
                  return 'calc(100vh - var(--header-height))';
                }
                return '100vh';
              } catch {
                return '100vh';
              }
            ]]]
        - max-height: |-
            [[[
              try {
                const screenMode = hass.states[variables.var_assistsat_entity]?.attributes?.screen_mode;
                if (screenMode === 'no_hide' || screenMode === 'hide_sidebar') {
                  return 'calc(100vh - var(--header-height))';
                }
                return '100vh';
              } catch {
                return '100vh';
              }
            ]]]
        - height: |-
            [[[
              try {
                const screenMode = hass.states[variables.var_assistsat_entity]?.attributes?.screen_mode;
                if (screenMode === 'no_hide' || screenMode === 'hide_sidebar') {
                  return 'calc(100vh - var(--header-height))';
                }
                return '100vh';
              } catch {
                return '100vh';
              }
            ]]]
        - aspect-ratio: |-
            [[[
              if (!window.viewAssistResponsive) return "16 / 9";

              return window.viewAssistResponsive.orientation === "portrait" ? "9 / 16" : "16 / 9";
            ]]]
        - background: |
            [[[
              if (variables.background != null) {
                return `center / cover no-repeat url(${variables.background})`
              } else if (variables.var_background != null) {
                  return `center / cover no-repeat url(${variables.var_background})`
              } else {
                return `center / cover no-repeat ${variables.background_color}`
              }
            ]]]
        - background-size: cover
        - border-radius: 0px
        - overflow: hidden
        - color: white
        - font-family: |-
            [[[
              return `"${variables.var_assistsat_entity_font_style}", sans-serif`;
            ]]]
        - font-weight: 300
        - position: relative
        - box-sizing: border-box
      custom_fields:
        title:
          - position: absolute
          - justify-self: start
          - align-self: start
          - z-index: 1
          - font-size: |-
              [[[
                return window.viewAssistResponsive?.orientation === "portrait" ? "220%" : "200%";
              ]]]
          - font-weight: 400
          - width: max-content
          - margin-left: 2%
          - margin-top: -4%
        status:
          - position: absolute
          - justify-self: end
          - align-self: end
          - justify-content: right
          - top: 0vh
          - right: 0vw
          - z-index: 1
  status_icons_content:
    custom_fields:
      title: '[[[ return variables.var_title ]]]'
      status:
        card:
          type: custom:layout-card
          layout_type: custom:horizontal-layout
          cards:
            - type: custom:layout-card
              layout_type: grid-layout
              layout:
                margin: 0
                card_margin: 0
                place-content: end
                grid-template-columns: |-
                  [[[
                    const baseIconSize = variables.var_assistsat_entity_responsive_status_icons_size;
                    return `repeat(auto-fit, minmax(${baseIconSize}, ${baseIconSize}))`;
                  ]]]
              cards: |-
                [[[{
                  const buttonList = [];

                  try {
                    const vaEntity = variables.var_assistsat_entity;
                    if (!vaEntity || !hass.states[vaEntity]) {
                      return [];
                    }

                    const statusIcons = hass.states[vaEntity].attributes.status_icons || [];
                    const menuItems = hass.states[vaEntity].attributes.menu_items || [];
                    const menuActive = hass.states[vaEntity].attributes.menu_active || false;
                    const currentView = variables.var_current_view;

                    const availableTemplates = new Set();
                    try {
                      const lovelace = document.querySelector("home-assistant")
                        ?.shadowRoot?.querySelector("home-assistant-main")
                        ?.shadowRoot?.querySelector("ha-drawer partial-panel-resolver ha-panel-lovelace")
                        ?.lovelace;

                      if (lovelace?.config?.button_card_templates) {
                        Object.keys(lovelace.config.button_card_templates).forEach(template => {
                          availableTemplates.add(template);
                        });
                      }
                    } catch (e) {
                    }

                    function isCurrentView(item) {
                      if (!currentView) return false;

                      if (typeof item === 'string' && !item.includes(':')) {
                        if (item === currentView) return true;

                        const homePath = hass.states[vaEntity]?.attributes?.home_screen || "/view-assist/clock";
                        const homeView = homePath.split('/').pop() || "clock";
                        const templateViewMap = {
                          "home": homeView
                        };
                        if (templateViewMap[item] === currentView) return true;
                        return false;
                      }

                      if (typeof item === 'string' && item.includes(':')) {
                        const parts = item.split('|');
                        const typeAndTarget = parts[0];
                        const [type, target] = typeAndTarget.split(':');

                        if (type === 'view') {
                          const viewName = target.includes('/') ?
                            target.split('/').pop() : target;
                          return viewName === currentView;
                        }
                      }

                      return false;
                    }

                    const addedItems = new Set();

                    function addIconToList(icon, isDynamicItem = false) {
                      if (icon === "menu") return;

                      if (isCurrentView(icon)) {
                        return;
                      }

                      let buttonConfig;

                      if (isDynamicItem) {
                        const type = icon.split(':')[0];
                        buttonConfig = {
                          type: "custom:button-card",
                          template: `dynamic_${type}_item`,
                          variables: {
                            menu_item: icon,
                            entity_id: vaEntity
                          }
                        };
                      } else {
                        if (availableTemplates.size > 0 && !availableTemplates.has(icon)) {
                          buttonConfig = {
                            type: "custom:button-card",
                            template: "icon_template",
                            icon: "mdi:alert-circle-outline",
                            name: icon,
                            styles: {
                              card: [{ "border": "2px dashed rgba(255, 152, 0, 0.8)" }],
                              icon: [{ "color": "rgba(255, 152, 0, 0.9)" }],
                              name: [{
                                "font-size": "0.7em",
                                "color": "rgba(255, 152, 0, 0.9)",
                                "padding-top": "4px"
                              }]
                            },
                            show_name: true,
                            tap_action: {
                              action: "none"
                            }
                          };
                        } else {
                          buttonConfig = {
                            type: "custom:button-card",
                            template: icon
                          };
                        }
                      }

                      const key = icon;
                      if (addedItems.has(key)) return;

                      addedItems.add(key);
                      buttonList.push(buttonConfig);
                    }

                    if (menuActive) {
                      const reversedMenuItems = [...menuItems].reverse();
                      reversedMenuItems.forEach(item => {
                        if (item !== "menu") {
                          if (item.includes(':')) {
                            addIconToList(item, true);
                          } else {
                            addIconToList(item, false);
                          }
                        }
                      });
                    }

                    statusIcons.forEach(icon => {
                      if (icon !== "menu") {
                        if (icon.includes(':')) {
                          addIconToList(icon, true);
                        } else {
                          addIconToList(icon, false);
                        }
                      }
                    });

                    if (hass.states[vaEntity].attributes.menu_config === "menu_enabled_button_visible" &&
                        !addedItems.has("menu")) {
                      buttonList.push({
                        type: "custom:button-card",
                        template: "menu"
                      });
                    }
                  } catch (e) {
                  }

                  return buttonList;
                }]]]
  status_icons_overlay:
    template:
      - variable_template
      - responsive_base
      - status_icons_content
    show_state: false
    show_icon: false
    show_name: false
    triggers_update: all
    styles:
      card: null
      custom_fields:
        status:
          - position: fixed
          - top: |-
              [[[
                try {
                  const screenMode = hass.states[variables.var_assistsat_entity]?.attributes?.screen_mode;
                  if (screenMode === 'no_hide' || screenMode === 'hide_sidebar') {
                    return 'calc(0vh + var(--header-height))';
                  }
                  return '0vh';
                } catch {
                  return '0vh';
                }
              ]]]
          - right: 0vw
          - z-index: 4
  navbar_overlay:
    template:
      - variable_template
      - responsive_base
    custom_fields:
      navbar:
        card:
          type: custom:view-assist-menu-card
          position: right
          reverse: true
  icon_template:
    template: variable_template
    color_type: card
    show_name: false
    size: 90%
    padding: 0px
    styles:
      card:
        - background-color: transparent
        - border-width: 0px
        - aspect-ratio: 1 / 1
      icon:
        - display: grid
        - color: white
  dynamic_view_item:
    template: icon_template
    icon: |-
      [[[
        const parts = variables.menu_item.split('|');
        const icon = parts.length > 1 ? parts[1] : 'view-dashboard';
        return `mdi:${icon}`;
      ]]]
    tap_action:
      action: call-service
      service: view_assist.navigate
      service_data:
        device: '[[[ return variables.entity_id; ]]]'
        path: |-
          [[[
            const parts = variables.menu_item.split('|');
            const typeAndTarget = parts[0];
            const target = typeAndTarget.split(':')[1];
            const base = variables.var_dashboard || '/view-assist';
            return target.startsWith('/') ? target : `${base}/${target}`;
          ]]]
  dynamic_entity_item:
    template: icon_template
    state_display: none
    entity: |-
      [[[
        const parts = variables.menu_item.split('|');
        const typeAndTarget = parts[0];
        return typeAndTarget.split(':')[1];
      ]]]
    icon: |-
      [[[
        const parts = variables.menu_item.split('|');
        const typeAndTarget = parts[0];
        const entityId = typeAndTarget.split(':')[1];
        const iconOptions = parts.length > 1 ? parts[1].split(',') : ['help-circle'];

        let icon = iconOptions[0];

        if (iconOptions.length > 1 && hass.states[entityId]) {
          const state = hass.states[entityId].state;

          if (state === 'off' && iconOptions[1]) {
            icon = iconOptions[1];
          }
        }

        return `mdi:${icon}`;
      ]]]
    styles:
      icon:
        - color: |-
            [[[
              const entityId = variables.menu_item
                .split("|")[0]
                .split(":")[1];

              return hass.states[entityId]?.state === "on"
                ? "var(--warning-color)"
                : "white";
            ]]]
    tap_action:
      action: toggle
      entity: |-
        [[[
          const parts = variables.menu_item.split('|');
          const typeAndTarget = parts[0];
          return typeAndTarget.split(':')[1];
        ]]]
    hold_action:
      action: more-info
      entity: |-
        [[[
          const parts = variables.menu_item.split('|');
          const typeAndTarget = parts[0];
          return typeAndTarget.split(':')[1];
        ]]]
  dynamic_service_item:
    template: icon_template
    icon: |-
      [[[
        const parts = variables.menu_item.split('|');
        const icon = parts.length > 1 ? parts[1] : 'cog';
        return `mdi:${icon}`;
      ]]]
    tap_action:
      action: call-service
      service: |-
        [[[
          const parts = variables.menu_item.split('|');
          const typeAndTarget = parts[0];
          return typeAndTarget.split(':')[1];
        ]]]
      service_data: {}
  mediaplayer:
    type: custom:button-card
    template: icon_template
    icon: mdi:volume-off
    tap_action:
      action: call-service
      service: media_player.volume_mute
      service_data:
        entity_id: '[[[ return variables.var_mediaplayer_device ]]]'
        is_volume_muted: false
  mic:
    type: custom:button-card
    template: icon_template
    icon: mdi:microphone-off
    tap_action:
      action: call-service
      service: homeassistant.turn_off
      service_data:
        entity_id: '[[[ return variables.var_mic_switch ]]]'
  hold:
    type: custom:button-card
    template: icon_template
    icon: mdi:hand-back-left
    tap_action:
      action: call-service
      service: view_assist.set_state
      service_data:
        mode: normal
        entity_id: '[[[ return variables.var_assistsat_entity ]]]'
  cycle:
    type: custom:button-card
    template: icon_template
    icon: mdi:sync
    tap_action:
      action: call-service
      service: view_assist.set_state
      service_data:
        mode: normal
        entity_id: '[[[ return variables.var_assistsat_entity ]]]'
  dnd:
    type: custom:button-card
    template: icon_template
    icon: mdi:minus-circle
    tap_action:
      action: call-service
      service: view_assist.set_state
      service_data:
        do_not_disturb: false
        entity_id: '[[[ return variables.var_assistsat_entity ]]]'
  weather:
    type: custom:button-card
    template: icon_template
    icon: mdi:weather-sunny
    tap_action:
      action: call-service
      service: view_assist.navigate
      service_data:
        device: '[[[ return variables.var_assistsat_entity ]]]'
        path: '[[[ return `${variables.var_dashboard}/weather` ]]]'
  home:
    type: custom:button-card
    template: icon_template
    icon: mdi:home
    tap_action:
      action: call-service
      service: view_assist.navigate
      service_data:
        device: '[[[ return variables.var_assistsat_entity ]]]'
        path: home
  menu:
    type: custom:button-card
    template: icon_template
    icon: mdi:menu
    tap_action:
      action: call-service
      service: view_assist.toggle_menu
      service_data:
        entity_id: '[[[ return variables.var_assistsat_entity ]]]'
        show: >-
          [[[ return
          !states[variables.var_assistsat_entity].attributes.menu_active ]]]
    hold_action:
      action: call-service
      service: view_assist.set_state
      service_data:
        entity_id: '[[[ return variables.var_assistsat_entity ]]]'
        mode: hold
  camera:
    type: custom:button-card
    template: icon_template
    icon: mdi:cctv
    tap_action:
      action: call-service
      service: view_assist.navigate
      service_data:
        device: '[[[ return variables.var_assistsat_entity ]]]'
        path: '[[[ return `${variables.var_dashboard}/camera` ]]]'
  music:
    type: custom:button-card
    template: icon_template
    icon: mdi:music
    tap_action:
      action: call-service
      service: view_assist.navigate
      service_data:
        device: '[[[ return variables.var_assistsat_entity ]]]'
        path: '[[[ return `${variables.var_dashboard}/music` ]]]'
  wake:
    type: custom:button-card
    template: icon_template
    icon: mdi:button-pointer
    tap_action:
      action: call-service
      service: button.press
      service_data:
        entity_id: '[[[ return variables.var_wake_switch ]]]'
views:
  - type: panel
    title: Clock
    path: clock
    cards:
      - type: custom:button-card
        variables:
          clockcardversion: 1.5.1
          var_background: >-
            [[[ try {if (hass.states[variables.var_assistsat_entity] &&
            hass.states[variables.var_assistsat_entity].attributes.mode ===
            "night") return ""; else return
            hass.states[variables.var_assistsat_entity] ?
            hass.states[variables.var_assistsat_entity].attributes.background :
            ""} catch {return ""}]]]
          var_font_color: >-
            [[[ try {if (hass.states[variables.var_assistsat_entity] &&
            hass.states[variables.var_assistsat_entity].attributes.mode ===
            "night") return "red"; else return "white";} catch {return "white";}
            ]]]
          var_font_color_night: >-
            [[[ try {if (hass.states[variables.var_assistsat_entity] &&
            hass.states[variables.var_assistsat_entity].attributes.mode ===
            "night") return "transparent"; else return "white";} catch {return
            "white";} ]]]
        template:
          - variable_template
          - responsive_base
          - body_template
        styles:
          grid:
            - grid-template-areas: |
                "title status"
                "time time"
                "date date"
            - grid-template-rows: |-
                [[[
                  if (window.viewAssistResponsive && window.viewAssistResponsive.orientation === "portrait") {
                    return "15vh 35vh 15vh";
                  } else {
                    return "15vh 50vh 15vh";
                  }
                ]]]
            - grid-template-columns: 1fr 1fr
          card:
            - background: >-
                [[[ return `center / cover no-repeat
                url(${variables.var_background})` ]]]
            - background-size: cover
            - background-color: black
            - position: relative
            - padding-right: clamp(70px, 9vw, 110px)
            - box-sizing: border-box
          custom_fields:
            title:
              - display: grid
              - color: '[[[ return variables.var_font_color_night ]]]'
            time:
              - display: grid
              - justify-self: center
              - align-self: center
              - z-index: 1
              - font-size: |-
                  [[[
                    if (window.viewAssistResponsive && window.viewAssistResponsive.orientation === "portrait") {
                      return "25vh";
                    } else {
                      return "55vh";
                    }
                  ]]]
              - font-weight: bold
              - opacity: >-
                  [[[ try {if (hass.states[variables.var_assistsat_entity] &&
                  hass.states[variables.var_assistsat_entity].attributes.mode
                  === "night") return "35%"; else return "100%";} catch {return
                  "100%";} ]]]
              - color: '[[[ return variables.var_font_color ]]]'
              - padding-top: 0
              - margin-top: 0
            date:
              - display: grid
              - justify-self: center
              - align-self: center
              - z-index: 1
              - font-size: |-
                  [[[
                    if (window.viewAssistResponsive && window.viewAssistResponsive.orientation === "portrait") {
                      return "8vh";
                    } else {
                      return "15vh";
                    }
                  ]]]
              - width: max-content
              - color: '[[[ return variables.var_font_color_night ]]]'
            night:
              - position: absolute
              - top: 0
              - min-height: 100%
              - width: 100%
              - overflow: hidden
              - display: >-
                  [[[ try {if (hass.states[variables.var_assistsat_entity] &&
                  hass.states[variables.var_assistsat_entity].attributes.mode
                  === "night") return "block"; else return "none";} catch {
                  return  "none"}
                   ]]]
              - z-index: 2
            shader:
              - position: absolute
              - top: 0
              - height: 100%
              - width: 100%
              - background-color: rgba(0,0,0,0.15)
        custom_fields:
          title:
            card:
              type: custom:button-card
              icon: '[[[ return variables.var_weather_icon ]]]'
              name: '[[[ return variables.var_weather_temperature ]]]'
              template: null
              tap_action:
                action: call-service
                service: view_assist.navigate
                service_data:
                  device: '[[[ return variables.var_assistsat_entity ]]]'
                  path: '[[[ return `${variables.var_dashboard}/weather` ]]]'
              styles:
                card:
                  - background-color: transparent
                  - border-width: 0px
                  - width: 100%
                  - top: -5%
                  - left: 5%
                grid:
                  - grid-template-areas: '"i n"'
                name:
                  - font-size: |-
                      [[[
                        if (window.viewAssistResponsive && window.viewAssistResponsive.orientation === "portrait") {
                          return "10vh";
                        } else {
                          return "15vh";
                        }
                      ]]]
                  - color: '[[[ return variables.var_font_color_night ]]]'
                icon:
                  - width: |-
                      [[[
                        if (window.viewAssistResponsive && window.viewAssistResponsive.orientation === "portrait") {
                          return "10vh";
                        } else {
                          return "13vh";
                        }
                      ]]]
                  - color: '[[[ return variables.var_font_color_night ]]]'
          time: '[[[ return variables.var_current_time ]]]'
          date: '[[[ return variables.var_date_short ]]]'
          night: ''
          shader: ''
  - type: panel
    title: Alarm
    path: alarm
    cards:
      - type: custom:button-card
        variables:
          alarmcardversion: 1.1.0
          var_background: >-
            [[[ try {if (variables.var_assistsat_entity &&
            hass.states[variables.var_assistsat_entity].attributes.mode ===
            "night") return ""; else return
            hass.states[variables.var_assistsat_entity].attributes.background}
            catch {return ""}]]]
        template:
          - variable_template
          - body_template
        styles:
          grid:
            - grid-template-areas: |
                "title status"
                "timers timers"
                "assist assist"
            - grid-template-rows: 15vh max-content 10vh
            - grid-template-columns: 1fr 1fr
          card:
            - background: |-
                [[[
                  return `center / cover no-repeat url(${variables.var_background})`;
                ]]]
            - background-size: cover
            - background-color: '#24292c'
          custom_fields:
            title:
              - padding: 10px 0
            timers:
              - justify-items: center
              - overflow-y: >-
                  [[[ return viewassist.config.timers.length > 1 ? 'scroll' :
                  'hidden' ]]]
              - max-height: 75vh
            shader:
              - position: absolute
              - top: 0
              - height: 100%
              - width: 100%
              - background-color: rgba(0,0,0,0.15)
        custom_fields:
          title: Timers & Alarms
          shader: ''
          timers:
            card: '[[[ return viewassist.helpers.timerCards() ]]]'
  - type: panel
    title: Alert
    path: alert
    cards:
      - type: custom:button-card
        variables:
          alertcardversion: 1.0.0
          var_icon: >-
            [[[ try {return
            hass.states[variables.var_assistsat_entity].attributes.alert_data['icon']}
            catch { return  "mdi:cancel"}]]]
          var_header: >-
            [[[ try {return
            hass.states[variables.var_assistsat_entity].attributes.alert_data['header']}
            catch { return  "Undefined"}]]]
          var_line1: >-
            [[[ try {return
            hass.states[variables.var_assistsat_entity].attributes.alert_data['line1']}
            catch { return  "Something went wrong"}]]]
          var_line2: >-
            [[[ try {return
            hass.states[variables.var_assistsat_entity].attributes.alert_data['line2']}
            catch { return  "Check your settings"}]]]
        template:
          - variable_template
          - body_template
        styles:
          grid:
            - grid-template-areas: |
                "title status"
                "alert alert"
                "assist assist"
            - grid-template-columns: 1.5fr 1.5fr
            - grid-template-rows: min-content max-content
          card:
            - background-color: '#059bf1'
            - background-size: cover
          custom_fields:
            title:
              - font-size: 6vh
              - font-family: Anton
            status:
              - font-size: 5vh
              - color: black
              - icon:
                  - color: black
                  - height: 90%
            alert:
              - align-self: center
              - justify-self: center
              - position: absolute
              - z-index: 2
              - width: 90%
        custom_fields:
          alert:
            card:
              type: custom:button-card
              custom_fields:
                header: '[[[ return variables.var_header ]]]'
                line1: '[[[ return variables.var_line1 ]]]'
                line2: '[[[ return variables.var_line2 ]]]'
                icon: >
                  [[[ return `<ha-icon icon="${variables.var_icon}"></ha-icon>`;
                  ]]]
              show_icon: false
              show_name: false
              styles:
                grid:
                  - grid-template-areas: |
                      "icon header"
                      "icon line1"
                      "icon line2"
                  - grid-template-columns: .5fr 1fr
                  - grid-template-rows: max-content max-content max-content
                card:
                  - justify-content: center
                  - align-items: center
                  - padding: 2%
                  - border-radius: 1vw
                  - background-color: '#059bf1'
                  - background-size: cover
                  - border: none
                custom_fields:
                  icon:
                    - align-self: center
                    - justify-self: end
                    - z-index: 2
                    - border-right: 2px solid black
                    - padding-right: 5%
                    - color: black
                  header:
                    - font-size: 7vh
                    - color: black
                    - justify-self: start
                    - padding-left: 10%
                    - font-weight: 500
                    - padding-top: 2em
                  line1:
                    - font-size: 7vh
                    - color: black
                    - justify-self: start
                    - padding-left: 10%
                    - padding-left: 10%
                  line2:
                    - font-size: 7vh
                    - color: black
                    - justify-self: start
                    - padding-left: 10%
                    - padding-bottom: 2em
  - type: panel
    title: Calendar
    path: calendar
    cards:
      - type: custom:button-card
        variables:
          calendarcardversion: 1.0.0
          var_list: >-
            [[[ try {return
            hass.states[variables.var_assistsat_entity].attributes.calendar_list}
            catch { return  ""}]]]
        template:
          - variable_template
          - body_template
        styles:
          card:
            - background: >-
                [[[ return `center / cover no-repeat
                url(${variables.background})` ]]]
            - background-size: cover
          custom_fields:
            message:
              - position: absolute
              - align-self: left
              - width: 100%
        custom_fields:
          message:
            card:
              entities: '[[[ return variables.var_list ]]]'
              days_to_show: 2
              day_spacing: 20px
              event_spacing: 8px
              today_indicator: mdi:star
              today_indicator_size: 12px
              weekday_font_size: 28px
              day_font_size: 52px
              month_font_size: 24px
              event_font_size: 28px
              time_font_size: 24px
              time_icon_size: 24px
              location_font_size: 24px
              location_icon_size: 24px
              type: custom:calendar-card-pro
              card_mod:
                style: |
                  ha-card {
                    text-align: left;
                  }
  - type: panel
    title: Camera
    path: camera
    cards:
      - type: custom:button-card
        variables:
          cameracardversion: 2.1.0
          var_url_params: |-
            [[[
              let queryString = '';
              const vaEntity = hass.states[variables.var_assistsat_entity];

              if (vaEntity?.attributes?.current_path) {
                const currentPath = vaEntity.attributes.current_path;
                queryString = currentPath.includes('?') ? currentPath.split('?')[1] : '';
              }

              if (!queryString && window.location.search) {
                queryString = window.location.search.substring(1);
              }

              return new URLSearchParams(queryString);
            ]]]
          var_camera: |-
            [[[
              const urlParams = variables.var_url_params;
              const urlCamera = urlParams.get('camera');
              const showAll = urlParams.get('show');

              if (showAll === 'all' || showAll === 'configured') {
                return "";
              }

              if (urlCamera) {
                return urlCamera;
              }

              const availableCameras = [];
              const cameraKeys = Object.keys(hass.states).filter(id => id.startsWith('camera.'));

              for (let i = 0; i < cameraKeys.length; i++) {
                const entityId = cameraKeys[i];
                const state = hass.states[entityId].state;
                if (state !== 'unavailable' && state !== 'unknown') {
                  availableCameras.push(entityId);
                }
              }

              if (availableCameras.length === 1) {
                return availableCameras[0];
              }

              return "";
            ]]]
          var_all_cameras: |-
            [[[
              const formatCameraName = (entityId) => {
                return entityId.replace('camera.', '').replace(/_/g, ' ');
              };

              const isCameraAvailable = (state) => {
                return state !== 'unavailable' && state !== 'unknown';
              };

              const urlParams = variables.var_url_params;
              const showType = urlParams.get('show');
              const configuredCameras = urlParams.get('cameras');

              if (showType === 'configured' && configuredCameras) {
                const cameras = [];
                const cameraEntityList = decodeURIComponent(configuredCameras).split(',');

                for (let i = 0; i < cameraEntityList.length; i++) {
                  const entityId = cameraEntityList[i].trim();
                  const entity = hass.states[entityId];
                  if (entity && isCameraAvailable(entity.state)) {
                    cameras.push({
                      entity_id: entityId,
                      name: entity.attributes.friendly_name || formatCameraName(entityId)
                    });
                  }
                }
                return cameras;
              }

              const viewAssistEntity = variables.var_assistsat_entity;

              if (viewAssistEntity && hass.states[viewAssistEntity]) {
                const cameraList = hass.states[viewAssistEntity].attributes.camera_list;

                if (Array.isArray(cameraList)) {
                  const cameras = [];

                  for (let i = 0; i < cameraList.length; i++) {
                    const entityId = cameraList[i];
                    const entity = hass.states[entityId];
                    if (entity && isCameraAvailable(entity.state)) {
                      cameras.push({
                        entity_id: entityId,
                        name: entity.attributes.friendly_name || formatCameraName(entityId)
                      });
                    }
                  }

                  if (cameras.length > 0) {
                    return cameras;
                  }
                }
              }

              const cameras = [];
              const cameraKeys = Object.keys(hass.states).filter(id => id.startsWith('camera.'));

              for (let i = 0; i < cameraKeys.length; i++) {
                const entityId = cameraKeys[i];
                const entity = hass.states[entityId];
                if (isCameraAvailable(entity.state)) {
                  cameras.push({
                    entity_id: entityId,
                    name: entity.attributes.friendly_name || formatCameraName(entityId)
                  });
                }
              }

              cameras.sort((a, b) => a.name.localeCompare(b.name));
              return cameras;
            ]]]
          var_responsive_columns: |-
            [[[
              return window.viewAssistResponsive?.orientation === "portrait" ? 1 : 2;
            ]]]
          var_timeout_seconds: |-
            [[[
              const timeout = variables.var_url_params.get('timeout');
              return timeout ? parseInt(timeout, 10) : 0;
            ]]]
          var_setup_camera_hold_mode: |-
            [[[
              const cleanup = () => {
                if (window.vaCameraHoldTimeout) {
                  clearTimeout(window.vaCameraHoldTimeout);
                }
                window.vaCameraHoldTimeout = null;
                window.vaCameraHoldModeSet = false;
                window.vaCameraPageKey = null;
                window.vaCameraTimeoutStart = null;
                window.vaCameraStoredPreviousMode = null;
                window.vaCameraRevertMode = null;
              };

              if (variables.var_current_view !== 'camera' || !variables.var_assistsat_entity) {
                cleanup();
                return '';
              }

              const currentPageKey = `${window.location.pathname}${window.location.search}`;

              if (window.vaCameraPageKey === currentPageKey && window.vaCameraHoldTimeout) {
                return '';
              }

              window.vaCameraPageKey = currentPageKey;
              const entityId = variables.var_assistsat_entity;
              const vaEntity = hass.states[entityId];

              const currentMode = vaEntity?.attributes?.mode || 'normal';
              const previousMode = currentMode === 'hold' ? 'normal' : currentMode;
              window.vaCameraStoredPreviousMode = previousMode;

              hass.callService('view_assist', 'set_state', {
                entity_id: entityId,
                mode: 'hold'
              });
              window.vaCameraHoldModeSet = true;

              if (window.vaCameraHoldTimeout) {
                clearTimeout(window.vaCameraHoldTimeout);
                window.vaCameraHoldTimeout = null;
              }

              const timeoutSeconds = variables.var_timeout_seconds;
              if (timeoutSeconds > 0) {
                window.vaCameraTimeoutStart = Date.now();

                window.vaCameraRevertMode = function() {
                  try {
                    let hassInstance = typeof hass !== 'undefined' ? hass :
                                      document.querySelector('home-assistant')?.hass;

                    if (hassInstance) {
                      hassInstance.callService('view_assist', 'set_state', {
                        entity_id: entityId,
                        mode: window.vaCameraStoredPreviousMode || 'normal'
                      });
                    }
                  } catch (error) {
                    console.warn('View Assist camera mode revert failed:', error);
                  } finally {
                    cleanup();
                  }
                };

                window.vaCameraHoldTimeout = setTimeout(window.vaCameraRevertMode, timeoutSeconds * 1000);
              }

              return '';
            ]]]
        template:
          - variable_template
          - responsive_base
          - body_template
        styles:
          grid:
            - grid-template-areas: |-
                [[[
                  return variables.var_camera ?
                    `"status status"
                     "camera camera"
                     "camera camera"` :
                    `"status status"
                     "camera_select camera_select"
                     "camera_select camera_select"`;
                ]]]
            - grid-template-columns: 1fr 1fr
            - grid-template-rows: min-content 1fr 1fr
          card:
            - background: >-
                [[[ return `center / cover no-repeat
                url(${variables.background})` ]]]
            - background-size: cover
            - border-radius: 0px
            - font-family: >-
                [[[ return `'${variables.var_assistsat_entity_font_style}',
                sans-serif`; ]]]
          custom_fields:
            camera:
              - display: '[[[ return variables.var_camera ? ''block'' : ''none'' ]]]'
              - height: 100%
              - width: 100%
            camera_select:
              - display: '[[[ return variables.var_camera ? ''none'' : ''block'' ]]]'
              - margin: 0.125rem
              - overflow-y: auto
              - max-height: 100vh
            back_button:
              - display: '[[[ return variables.var_camera ? ''block'' : ''none'' ]]]'
              - position: absolute
              - left: 1rem
              - top: 1rem
              - z-index: 1
        custom_fields:
          _hold_mode_init: '[[[ return variables.var_setup_camera_hold_mode ]]]'
          back_button:
            card:
              type: custom:button-card
              icon: mdi:arrow-left
              show_name: false
              styles:
                card:
                  - background: rgba(0,0,0,0.6)
                  - border-radius: 50%
                  - width: 3rem
                  - height: 3rem
                  - padding: 0
                icon:
                  - color: white
                  - width: 1.75rem
                  - height: 1.75rem
              grid:
                - grid-template-areas: i
                - justify-items: center
                - align-items: center
              tap_action:
                action: call-service
                service: view_assist.navigate
                service_data:
                  device: '[[[ return variables.var_assistsat_entity ]]]'
                  path: '[[[ return `${variables.var_dashboard}/camera` ]]]'
          camera:
            card:
              type: picture-entity
              entity: '[[[ return variables.var_camera ]]]'
              camera_view: live
              show_name: false
              show_state: false
              tap_action:
                action: more-info
          camera_select:
            card:
              type: grid
              columns: '[[[ return variables.var_responsive_columns ]]]'
              square: false
              cards: |-
                [[[
                  const cameras = variables.var_all_cameras;
                  if (!Array.isArray(cameras) || cameras.length === 0) {
                    return [{
                      type: "markdown",
                      content: "No cameras available",
                      card_mod: {
                        style: `
                          ha-card {
                            background: rgba(0,0,0,0.6);
                            color: white;
                            padding: 2rem;
                            border-radius: 0.5rem;
                            text-align: center;
                          }
                        `
                      }
                    }];
                  }

                  const timeoutParam = variables.var_timeout_seconds > 0 ? `&timeout=${variables.var_timeout_seconds}` : '';

                  return cameras.map(camera => ({
                    type: "picture-entity",
                    entity: camera.entity_id,
                    name: camera.name,
                    show_state: false,
                    show_name: true,
                    camera_view: "auto",
                    tap_action: {
                      action: "call-service",
                      service: "view_assist.navigate",
                      service_data: {
                        device: variables.var_assistsat_entity,
                        path: `${variables.var_dashboard}/camera?camera=${camera.entity_id}${timeoutParam}`
                      }
                    },
                    card_mod: {
                      style: `
                        ha-card {
                          border-radius: 0.5rem;
                        }
                      `
                    }
                  }));
                ]]]
  - type: panel
    title: Info
    path: info
    cards:
      - type: custom:button-card
        variables:
          background: /view_assist/dashboard/infobackground.png
          infocardversion: 1.0.0
        template:
          - variable_template
          - body_template
        styles:
          grid:
            - grid-template-areas: |
                "title status"
                "message message"
                "assist assist"
            - grid-template-rows: min-content 4fr min-content
            - grid-template-columns: 1fr 1fr
          card:
            - background: >-
                [[[ return `center / cover no-repeat
                url(${variables.background})` ]]]
            - background-size: cover
          custom_fields:
            message:
              - font-size: '[[[ return variables.var_font_size ]]]'
              - position: relative
              - padding: 10px
              - border-radius: 10px
              - width: 95%
              - text-align: start
              - text-wrap: wrap
              - justify-content: center
              - align-self: center
              - padding: 2%
        custom_fields:
          message: '[[[ return variables.var_message ]]]'
  - type: panel
    title: Infopic
    path: infopic
    cards:
      - type: custom:button-card
        variables:
          background: /view_assist/dashboard/infobackground.png
          infopiccardversion: 1.0.0
        template:
          - variable_template
          - body_template
        styles:
          grid:
            - grid-template-areas: |
                "title status"
                "image message"
                "assist assist"
            - grid-template-rows: min-content 4fr min-content
            - grid-template-columns: 1fr 2fr
          card:
            - background: >-
                [[[ return `center / cover no-repeat
                url(${variables.background})` ]]]
            - background-size: cover
          custom_fields:
            image:
              - align-self: center
              - justify-self: center
              - object-fit: contain
              - width: 75%
            message:
              - font-size: '[[[ return variables.var_font_size ]]]'
              - position: relative
              - padding: 10px
              - border-radius: 10px
              - width: 95%
              - text-align: start
              - text-wrap: wrap
              - justify-content: center
              - align-self: center
              - padding: 2%
        custom_fields:
          image:
            card:
              type: picture
              image: >-
                [[[ return
                states[variables.var_assistsat_entity].attributes.image; ]]]
          message: '[[[ return variables.var_message ]]]'
  - type: panel
    title: Intent
    path: intent
    cards:
      - type: custom:button-card
        variables:
          intentcardversion: 1.0.1
        template:
          - variable_template
          - body_template
        styles:
          card:
            - background-color: '#000000'
            - border-width: 0px
            - border-radius: 0px
          custom_fields:
            message:
              - position: absolute
              - width: 100%
              - align-self: middle
              - justify-self: center
        custom_fields:
          title: ''
          message:
            card:
              type: custom:layout-card
              layout_type: custom:masonry-layout
              layout:
                max_cols: 3
              cards: >-
                [[[ try {return
                hass.states[variables.var_assistsat_entity].attributes.intent_entities}
                catch { return ""}]]]
  - type: panel
    title: List
    path: list
    cards:
      - type: custom:button-card
        variables:
          listcardversion: 1.0.2
          var_list: >-
            [[[ try {return
            hass.states[variables.var_assistsat_entity].attributes.list} catch {
            return  "todo.shopping_list"}]]]
          background: /view_assist/dashboard/infobackground.png
        template:
          - variable_template
          - body_template
        styles:
          card:
            - background: >-
                [[[ return `center / cover no-repeat
                url(${variables.background})` ]]]
            - background-size: cover
        custom_fields:
          message:
            card:
              type: todo-list
              entity: '[[[ return variables.var_list ]]]'
              card_mod:
                style:
                  .: |
                    ha-card {
                      background-color: transparent;
                      box-shadow: none;
                      border: none;
                      position: absolute;
                      top: 15vh; /* Play with this value to adjust top spacing */
                      height: 80vh; /* Limit the vertical size to allow scrolling */
                      /* flex-direction: column-reverse;  <-- Enable this to auto scroll to the end of the list */
                      overflow-y: auto;
                      padding: 1rem;
                      display: flex;
                      flex-wrap: wrap;
                      align-content: flex-start;
                    }

                    ha-check-list-item {
                      color: white;
                      font-size: 3vw;
                      line-height: 1.6;
                    }

                    ha-card.type-todo-list div.header,
                    ha-card.type-todo-list .addRow,
                    ha-card.type-todo-list div.divider,
                    ha-check-list-item.editRow.completed {
                      display: none;
                    }
  - type: panel
    title: Locate
    path: locate
    cards:
      - type: custom:button-card
        variables:
          locatecardversion: 1.0.0
        template:
          - variable_template
          - body_template
        styles:
          grid:
            - grid-template-areas: |
                "title status"
                "map map"
                "assist assist"
            - grid-template-columns: 1.5fr 1.5fr
            - grid-template-rows: min-content max-content
          card:
            - background: black
            - background-size: cover
          custom_fields:
            map:
              - align-self: center
              - width: 100%
              - height: 100%
              - position: absolute
              - justify-content: center
              - z-index: 1
            location:
              - align-self: center
              - justify-self: center
              - position: absolute
              - z-index: 2
              - top: 70vh
              - width: 90%
            hold_card:
              - align-self: center
              - justify-self: center
              - position: absolute
              - width: 80vw
              - z-index: 3
        custom_fields:
          title: >-
            [[[ return new Date().toLocaleTimeString([], { hour: "numeric",
            minute: "2-digit" }).toLowerCase(); ]]]
          map:
            card:
              type: map
              entities:
                - entity: >-
                    [[[ try {return
                    hass.states[variables.var_assistsat_entity].attributes.locate_data['person']}
                    catch { return ""}]]]
              theme_mode: |-
                [[[ try {
                  var var_map_mode = hass.states[variables.var_assistsat_entity].attributes.locate_data.map_mode;
                  return `${var_map_mode}`}
                catch { return "dark"}]]]
              default_zoom: 15
              aspect_ratio: 1.5/1
              auto_fit: true
              fit_zones: false
              card_mod:
                style:
                  ha-map $ ha-entity-marker $: |
                    .marker {
                      color: white !important;
                      background-color: #03a9f4 !important;
                      opacity: 80% !important;
                      font-size: 3vw !important;
                      font-weight: bold !important;
                      height: 5vw !important;
                      width: 5vw !important;
                     }
                  ha-map$: |
                    .leaflet-control-attribution {
                      visibility: hidden;
                     }
                    .leaflet-control-zoom {
                      right: -11px;
                      top: 24vh;
                      transform: scale(1.8)
                     }
                  ha-icon-button$: |
                    mwc-icon-button[title="Reset focus"]{
                      --mdc-icon-size: 65px;
                      right: -10px !important;
                      position: relative !important;
                      display: flex !important
                    }
          location:
            card:
              type: custom:button-card
              custom_fields:
                location_text: |-
                  [[[ try {
                    var var_location_text = hass.states[variables.var_assistsat_entity].attributes.locate_data.location_text;
                    return `${var_location_text}`}
                  catch { return ""}]]]
                updated: |-
                  [[[ try {
                    var var_location_last_change = hass.states[variables.var_assistsat_entity].attributes.locate_data.location_last_change;
                    return `${var_location_last_change}`}
                  catch { return ""}]]]
              show_icon: false
              show_name: false
              styles:
                grid:
                  - grid-template-areas: |
                      "location_text"
                      "updated"
                  - grid-template-columns: 1fr
                  - grid-template-rows: 1fr min-content min-content
                card:
                  - justify-content: center
                  - align-items: center
                  - padding: 2%
                  - border-radius: 1vw
                  - background-color: grey
                  - border: none
                  - filter: opacity(75%)
                custom_fields:
                  location_text:
                    - font-size: 6vh
                    - color: black
                    - text-wrap: wrap
                    - font-weight: bold
                    - text-align: center
                  updated:
                    - font-size: 4vh
                    - color: black
          hold_card:
            card:
              type: custom:button-card
              show_icon: false
              show_name: false
              tap_action:
                action: call-service
                service: python_script.set_state
                service_data:
                  entity_id: '[[[ return variables.var_assistsat_entity ]]]'
                  mode: hold
              double_tap_action:
                action: call-service
                service: python_script.set_state
                service_data:
                  entity_id: '[[[ return variables.var_assistsat_entity ]]]'
                  mode: normal
              styles:
                card:
                  - top: 10vh
                  - border-radius: 1vw
                  - background-color: transparent
                  - height: 90vh
                  - width: 100vw
                  - border: none
  - type: panel
    title: Music
    path: music
    cards:
      - type: custom:button-card
        variables:
          musiccardversion: 1.1.1
          var_musicplayer_device: |-
            [[[
              var assistbid = localStorage.getItem("view_assist_sensor");
              var musicdevice = hass.states[assistbid].attributes.musicplayer_device;
              return `${musicdevice}`
            ]]]
        template:
          - variable_template
          - body_template
        styles:
          card:
            - background-color: black;
          custom_fields:
            message:
              - font-size: '[[[ return variables.var_font_size ]]]'
              - align-self: center
              - justify-self: center
              - width: 100%
              - height: 100%
              - position: absolute
              - justify-content: center
              - text-align: start
        custom_fields:
          title: '[[[ return variables.var_current_time ]]]'
          message:
            card:
              type: custom:mod-card
              card:
                type: media-control
                name: ' '
                entity: '[[[ return variables.var_musicplayer_device ]]]'
              style: |
                ha-card {
                  width: 100vw !important;
                  height: 100vh !important;
                  margin: 0 !important;
                  padding: 0 !important;
                  box-shadow: none !important;
                  --paper-card-background-color: transparent !important;
                  --mdc-icon-size: 0px;
                }
  - type: panel
    title: Sports
    path: sports
    cards:
      - type: custom:button-card
        variables:
          sportsversion: 1.1.0
          var_teamtracker_device: >-
            [[[ try {return
            hass.states[variables.var_assistsat_entity].attributes.team_tracker}
            catch { return  "sensor.team_tracker"}]]]
        template:
          - variable_template
          - body_template
        styles:
          card:
            - background-color: '#1c1c1c'
            - background-size: cover
          custom_fields:
            message:
              - font-size: '[[[ return variables.var_font_size ]]]'
              - align-self: center
              - justify-self: center
              - width: 100%
              - position: absolute
              - justify-content: center
              - text-align: start
        custom_fields:
          title: ''
          message:
            card:
              type: custom:teamtracker-card
              entity: '[[[ return variables.var_teamtracker_device ]]]'
  - type: panel
    title: Thermostat
    path: thermostat
    cards:
      - type: custom:button-card
        variables:
          thermostatcardversion: 1.0.2
          var_climate_device: >-
            [[[ try {return
            hass.states[variables.var_assistsat_entity].attributes.climate}
            catch { return ""}]]]
        template:
          - variable_template
          - body_template
        styles:
          card:
            - background-color: '#1c1c1c'
            - background-size: cover
          custom_fields:
            message:
              - font-size: '[[[ return variables.var_font_size ]]]'
              - position: absolute
              - padding: 10px
              - border-radius: 10px
              - width: 95%
              - text-align: start
              - text-wrap: wrap
              - justify-content: center
              - align-self: center
              - padding: 2%
        custom_fields:
          title: ''
          message:
            card:
              type: thermostat
              entity: '[[[ return variables.var_climate_device ]]]'
              name: ' '
              card_mod:
                style:
                  .: |
                    .content {
                      transform: scale(1.8);
                    }
                  ha-icon-button:
                    $:
                      mwc-icon-button:
                        $:
                          button: |
                            mwc-ripple {
                              display: none;
                            }
  - type: panel
    title: Weather
    path: weather
    cards:
      - type: custom:button-card
        variables:
          var_weather_entity: |-
            [[[
              var assistbid = localStorage.getItem("view_assist_sensor") ?? variables.default_satellite;
              var weather_entity = hass.states[assistbid].attributes.weather_entity;
              return `${weather_entity}`
            ]]]
          var_forecast_type: daily
          weathercardversion: 1.1.0
        template:
          - variable_template
          - body_template
        styles:
          grid:
            - grid-template-areas: |
                "title status"
                "message message"
                "assist assist"
            - grid-template-rows: min-content max-content min-content
            - grid-template-columns: 1fr 1fr
          card:
            - background-color: '#059bf1'
            - border-width: 0px
            - border-radius: 0px
            - padding-right: clamp(70px, 9vw, 110px)
            - box-sizing: border-box
          custom_fields:
            message:
              - position: relative
              - height: 100vdh
              - text-align: start
              - text-wrap: wrap
              - justify-content: center
              - align-self: center
              - padding: -10%
        custom_fields:
          title: ''
          message:
            card:
              type: weather-forecast
              entity: '[[[ return variables.var_weather_entity ]]]'
              forecast_type: '[[[ return variables.var_forecast_type ]]]'
              card_mod:
                style:
                  .: |
                    ha-card { background: #059bf9}
                    ha-card.type-weather-forecast {
                      justify-content: start !important;
                    }
                    ha-card.type-weather-forecast>div.content {
                      div {
                        padding-right: 10px;
                      }
                    }
                    ha-card.type-weather-forecast>div.content {
                      display: flex;​
                    }
                  ha-card.type-weather-forecast>div.content: |
                    svg {
                      width: 15vw !important;
                      height: 15vh !important;
                      flex: unset !important;
                    }
                  ha-card.type-weather-forecast>div.forecast: |
                    div {
                      padding-top: 2vh;
                      font-size: 6vh;
                      color: white !important;
                      justify-content: space-evenly !important;
                    }
                  ha-card.type-weather-forecast>div.content>div.info:
                    .: |
                      div.name-state {
                        display: flex;
                        flex-direction: column;
                      }
                    div.name-state:
                      .: |
                        div.state {
                          font-size: 10vh !important;
                        }
                        div.name {
                          display: none;
                          max-height: 0px !important;
                        }
                    div.temp-attribute: |
                      .temp {
                        font-size: 8vh !important;
                        padding-bottom: 0px;
                      }
                      .attribute {
                        font-size: 5vh !important;
                      }
                      .temp span {
                        font-size: 7vh !important;
                        margin-left: -1vh;
                      }
                      .templow {
                        padding-top: 0px;
                        text-align: right;
                      }
  - type: panel
    title: Webpage
    path: webpage
    cards:
      - type: custom:button-card
        variables:
          webpageversion: 1.1.1
          var_url: >-
            [[[ try {return
            hass.states[variables.var_assistsat_entity].attributes.url} catch {
            return "https://www.home-assistant.io/"}]]]
        template:
          - variable_template
          - body_template
        styles:
          card:
            - background-color: '#00000'
            - border-width: 0px
            - border-radius: 0px
          custom_fields:
            message:
              - font-size: '[[[ return variables.var_font_size ]]]'
              - align-self: center
              - justify-self: center
              - height: 100%
              - width: 100%
              - position: absolute
              - justify-content: center
              - text-align: start
        custom_fields:
          message:
            card:
              type: iframe
              url: '[[[ return variables.var_url ]]]'
              aspect_ratio: 50%

Lovelace dashboard for a room

Lovelace dashboard for an Amazon Echo Show 5 Gen 2
A Lovelace dashboard with a simple set of controls for the room. Its side menu reads the active device from view_assist_sensor in the browser’s local storage, then loads the menu_items configured on that View Assist entity. This keeps the menu entries synchronized with the modified View Assist dashboard.

The example supports the predefined home, weather, camera, and music items, along with the view:, entity:, and service: formats described above. You will need to customize the room controls and entity IDs for your devices. The main reusable part is the side-menu card configuration.

LoveLace Dashboard for Room
views:
  - type: sections
    sections:
      - type: grid
        cards:
          - type: vertical-stack
            cards:
              - type: tile
                entity: climate.hvac_study_hvac_study
                features:
                  - type: climate-hvac-modes
                  - type: target-temperature
                  - type: climate-fan-modes
                  - type: climate-swing-modes
                    style: dropdown
            grid_options:
              columns: 12
              rows: auto
          - type: vertical-stack
            cards:
              - type: tile
                entity: fan.skyfan_study_skyfan_study_fan
                name: Fan
                vertical: false
                features:
                  - type: fan-speed
                  - type: fan-direction
                features_position: bottom
              - type: tile
                entity: light.skyfan_study_skyfan_study_light
                name: Light
                vertical: false
                features:
                  - type: light-brightness
                  - type: light-color-favorites
                features_position: bottom
            grid_options:
              columns: 9
              rows: auto
        column_span: 3
      - type: grid
        cards:
          - type: custom:view-assist-menu-card
            position: right
            reverse: true
    badges: []
    max_columns: 4
    cards: []
    title: display