[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 });
  }
}

3 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

3 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>).

I know it’s solved, and it’s kind of an older thread, but I have yet another method for detection changes without depending on language strings. This method hooks into the websocket, and listens to the update message being pushed to the frontend:

function autoReloadHandler(e) {
  let evt = JSON.parse(e.data);
  if (!Array.isArray(evt)) {
    evt = new Array(evt);
  }     
  evt.forEach(e => {
    if (e.type === 'event' && e.event.event_type === 'lovelace_updated') {
      if (document.location.pathname.startsWith('/' + e.event.data.url_path)) {
        setTimeout(() => document.location.reload(), 500);
      }                 
    }           
  });   
}

window.hassConnection.then(t => t.conn.socket.addEventListener("message", autoReloadHandler));

console.info(
  `%cAUTO-RELOAD\n%cVersion: 0.0.1`,
  "color: green; font-weight: bold;",
  ""
);

Installation is identical to the other scripts, e.g. as the post above.

3 Likes

Hi.
With 2025.05 querying DOM on timer solution not working anymore, but this approach does work.
There is small problem, I need to suport two languages, English & Croatian.
Since a really don’t know how to create bundle, I changed string ā€˜REFRESH’ with Croatian ā€˜OSVJEŽITI’ in raw output and it works on Croatian, but I would like to have working solution on both languages.
Can you help me, please?
Best regards,

Amazing! That’s a neat idea. I adapted it to handle the Lovelace home page and also not to refresh the page being edited…

function autoReloadHandler(e) {
  // Decide if we're in edit mode and ignore any events
  const ha = document.querySelector("home-assistant");
  if (!ha || !ha.shadowRoot) return;
  const ham = ha.shadowRoot.querySelector("home-assistant-main");
  if (!ham || !ham.shadowRoot) return;
  const had = ham.shadowRoot.querySelector("ha-drawer");
  if (!had || !had.shadowRoot) return;
  const hpl = ham.shadowRoot.querySelector("ha-panel-lovelace");
  if (!hpl || !hpl.shadowRoot) return;
  const huiroot = hpl.shadowRoot.querySelector("hui-root");
  if (!huiroot || !huiroot.shadowRoot) return;
  const editMode = huiroot.shadowRoot.querySelector("div.edit-mode");
  if (editMode !== null) return;

  let evt = JSON.parse(e.data);
  if (!Array.isArray(evt)) {
    evt = new Array(evt);
  }

  // Handle update, including lovelace home page
  evt.forEach(e => {
    if (e.type === 'event' && e.event.event_type === 'lovelace_updated') {
      if (document.location.pathname.startsWith('/' + e.event.data.url_path) || ((document.location.pathname === '/lovelace/home') && ( e.event.data.url_path === null ))) {
        setTimeout(() => document.location.reload(), 500);
      }                 
    }           
  });   
}

window.hassConnection.then(t => t.conn.socket.addEventListener("message", autoReloadHandler));
  
console.info(
  `%cAUTO-RELOAD\n%cVersion: 0.0.2`,
  "color: green; font-weight: bold;",
  ""
);

i’m completely new here. where does you code go @gazoodle ?

This works beautifully, thank you.