[SOLVED] How can I get my dashboard to refresh automatically, instead of showing the "Refresh?" prompt?

On tablets I would recommend the fully kiosk browser app. It allows for remote reload. If you need the sensors from the app, you can of course still let it run in the background.

Exactly. Itā€™s also advisable to clear cache, which is also available remotely.
Rest commands are:
clear cache:
url: ā€œhttp://192.168.x.y:2323/?cmd=clearCache&password=yourpasswordā€
reload:
url: ā€œhttp://192.168.x.y:2323/?cmd=loadStartUrl&password=yourpasswordā€

I have a script which combines above two each time i refresh my screen. When ā€œmessingā€ with dashbvoard, updatingā€¦etcā€¦ itā€™s always recommended to clear cache to avoid unwanted wrong displaying.

You do need paid version of fully, though, or warning text will appear when you enable remote connection. And, as mentioned often: fixed IP is a must. Donā€™t mess with DHCP in HA, it brings nothing but problems.

I would use Fully, but itā€™s 100% unencrypted command and control traffic.

Not necessarily true as you can enable SSL support as described here: Fully Kiosk Browser & App Lockdown Help - Fully Kiosk Browser :slight_smile: This also applies to the REST-API

1 Like

Oh! Useful to know. Thanks for the correction.

This is a somewhat old thread, but before I found it I put together my own script to do this. Looks like most of the solutions here are querying the DOM on a timer. I took a different approach. I havenā€™t profiled it yet to see if itā€™s more performant.

Note that it relies on the home-assistant-query-selector package.

import { HAQuerySelector, HAQuerySelectorEvent, type OnListenDetail } from 'home-assistant-query-selector';

const NOTIFICATION_MANAGER_LOCAL_NAME: string = 'notification-manager';
const HA_TOAST_LOCAL_NAME: string = 'ha-toast';
const HA_BUTTON_LOCAL_NAME: string = 'ha-button';
const BUTTON_LOCAL_NAME: string = 'button';
const REFRESH_BUTTON_ARIA_LABEL_NORMALIZED: string = 'REFRESH';

function handleToastButton(buttonElement: ShadowRoot | Element): void {
  const buttonMutationObserver: MutationObserver = new MutationObserver(
    (buttonMutations: MutationRecord[]) => {
      for (const { addedNodes } of buttonMutations) {
        addedNodes.forEach((addedNode: Node) => {
          const { localName } = addedNode as Element;
          if (localName === BUTTON_LOCAL_NAME) {
            const button: HTMLButtonElement = addedNode as HTMLButtonElement;
            if (button.ariaLabel?.toUpperCase() === REFRESH_BUTTON_ARIA_LABEL_NORMALIZED) {
              buttonMutationObserver.disconnect(); // Found what we want. We're done watching the button

              // We could simulate clicking the button, but that doesn't always reload resources.
              // button.click();

              location.reload();
            }
          }
        });
      }
    }
  );

  buttonMutationObserver.observe(buttonElement, { childList: true });
}

function handleToastMutations(
  toastMutations: MutationRecord[],
  toastMutationObserver: MutationObserver
): void {
  for (const { addedNodes } of toastMutations) {
    addedNodes.forEach((addedNode: Node) => {
      const { localName, shadowRoot } = addedNode as Element;
      if (shadowRoot && localName === HA_BUTTON_LOCAL_NAME) {
        handleToastButton(shadowRoot);
        toastMutationObserver.disconnect(); // Found what we want. We're done watching the toast
      }
    });
  }
}

function handleNotificationManagerMutations(
  notificationManagerMutations: MutationRecord[],
  notificationManagerMutationObserver: MutationObserver
): void {
  for (const { addedNodes } of notificationManagerMutations) {
    addedNodes.forEach((addedNode: Node) => {
      const addedElement = addedNode as Element;
      const { localName, shadowRoot } = addedElement;
      if (shadowRoot && localName === HA_TOAST_LOCAL_NAME) {
        const haButton: Element | null = addedElement.querySelector(HA_BUTTON_LOCAL_NAME);
        if (haButton) {
          // The toast element doesn't put the button in its shadow DOM for some reason
          handleToastButton(haButton.shadowRoot ?? haButton);
        } else {
          // If the toast didn't contain the button, watch it in case the button eventually gets put in the shadow DOM
          const toastMutationMutationObserver: MutationObserver = new MutationObserver(handleToastMutations);
          toastMutationMutationObserver.observe(addedNode, { childList: true });
        }

        notificationManagerMutationObserver.disconnect(); // Found what we want. We're done watching the notification manager
      }
    });
  }
}

const homeAssistantRootElementMutationObserver: MutationObserver = new MutationObserver(
  (homeAssistantRootElementMutations: MutationRecord[]) => {
    for (const { addedNodes } of homeAssistantRootElementMutations) {
      addedNodes.forEach((addedNode: Node) => {
        console.log(addedNode);
        const { localName, shadowRoot } = addedNode as Element;
        if (shadowRoot && localName === NOTIFICATION_MANAGER_LOCAL_NAME) {
          const notificationManagerMutationObserver: MutationObserver = new MutationObserver(
            handleNotificationManagerMutations
          );

          notificationManagerMutationObserver.observe(shadowRoot, {
            childList: true
            // Ideally, we'd just watch the subtree, but MutationObserver doesn't observe inside sub-shadow roots
            // subtree: true
          });
        }
      });
    }
  }
);

const instance: HAQuerySelector = new HAQuerySelector();
instance.addEventListener(HAQuerySelectorEvent.ON_LISTEN, onListenHandler);
instance.listen();

function onListenHandler({
  detail: {
    HOME_ASSISTANT: {
      selector: {
        $: { element: haMainElementAsync }
      }
    }
  }
}: CustomEvent<OnListenDetail>): void {
  // This should never throw
  // eslint-disable-next-line no-void
  void initializeAsync(haMainElementAsync);
}

async function initializeAsync(
  // eslint-disable-next-line @rushstack/no-new-null
  haMainElementAsync: Promise<ShadowRoot | null>
): Promise<void> {
  const haMainElement: ShadowRoot | null = await haMainElementAsync;
  if (haMainElement) {
    homeAssistantRootElementMutationObserver.observe(haMainElement, { childList: true });
  }
}

2 Likes

Thank you so much. This have seemed to fixed my custom_card css styles not updating dynamic colors after reload. This was some good hacking :+1:

Where do you actually place this code?

It needs to be transpiled to JavaScript and bundled (or home-assistant-query-selector needs to be abstraced out, or referenced as a module with its dependency fulfilled), and then it that output gets placed in the config/www folder on HomeAssistant.

I bundled everything with Webpack and I have a CI pipeline to deploy it to my HA instance.

If you want the raw output of that, here it is: Script for automatically refreshing a Home Assistant dashboard on change Ā· GitHub

2 Likes

So make a new file in config/www folder call it ha-auto-refresh.js copy the raw contents of your link ha-auto-refresh.js and it will work?

Yes, you can stick the file directly under config/www. Then you reference it on dashboards by editing the dashboard, clicking the three vertical dots in the upper-right corner, selecting ā€œManage resourcesā€, and then adding a JavaScript resource with the URL /local/ha-auto-refresh.js (i.e. - /local/<path to the file under www>).