Change logic for default panel

This commit is contained in:
Paul Bottein
2025-11-12 17:24:51 +01:00
parent 13146575a0
commit 2044deefb8
7 changed files with 123 additions and 19 deletions

View File

@@ -14,12 +14,17 @@ declare global {
interface FrontendUserData { interface FrontendUserData {
core: CoreFrontendUserData; core: CoreFrontendUserData;
sidebar: SidebarFrontendUserData; sidebar: SidebarFrontendUserData;
default_panel: string; default_panel?: string;
}
interface FrontendSystemData {
default_panel?: string;
} }
} }
export type ValidUserDataKey = keyof FrontendUserData; export type ValidUserDataKey = keyof FrontendUserData;
export type ValidSystemDataKey = keyof FrontendSystemData;
export const fetchFrontendUserData = async < export const fetchFrontendUserData = async <
UserDataKey extends ValidUserDataKey, UserDataKey extends ValidUserDataKey,
>( >(
@@ -60,3 +65,46 @@ export const subscribeFrontendUserData = <UserDataKey extends ValidUserDataKey>(
key: userDataKey, key: userDataKey,
} }
); );
export const fetchFrontendSystemData = async <
SystemDataKey extends ValidSystemDataKey,
>(
conn: Connection,
key: SystemDataKey
): Promise<FrontendSystemData[SystemDataKey] | null> => {
const result = await conn.sendMessagePromise<{
value: FrontendSystemData[SystemDataKey] | null;
}>({
type: "frontend/get_system_data",
key,
});
return result.value;
};
export const saveFrontendSystemData = async <
SystemDataKey extends ValidSystemDataKey,
>(
conn: Connection,
key: SystemDataKey,
value: FrontendSystemData[SystemDataKey]
): Promise<void> =>
conn.sendMessagePromise<undefined>({
type: "frontend/set_system_data",
key,
value,
});
export const subscribeFrontendSystemData = <
SystemDataKey extends ValidSystemDataKey,
>(
conn: Connection,
systemDataKey: SystemDataKey,
onChange: (data: { value: FrontendSystemData[SystemDataKey] | null }) => void
) =>
conn.subscribeMessage<{ value: FrontendSystemData[SystemDataKey] | null }>(
onChange,
{
type: "frontend/subscribe_system_data",
key: systemDataKey,
}
);

View File

@@ -4,10 +4,10 @@ import type { HomeAssistant, PanelInfo } from "../types";
/** Panel to show when no panel is picked. */ /** Panel to show when no panel is picked. */
export const DEFAULT_PANEL = "lovelace"; export const DEFAULT_PANEL = "lovelace";
export const getStorageDefaultPanelUrlPath = (): string => { export const getStorageDefaultPanelUrlPath = (): string | undefined => {
const defaultPanel = window.localStorage.getItem("defaultPanel"); const defaultPanel = window.localStorage.getItem("defaultPanel");
return defaultPanel ? JSON.parse(defaultPanel) : DEFAULT_PANEL; return defaultPanel ? JSON.parse(defaultPanel) : undefined;
}; };
export const setDefaultPanel = ( export const setDefaultPanel = (
@@ -17,10 +17,15 @@ export const setDefaultPanel = (
fireEvent(element, "hass-default-panel", { defaultPanel: urlPath }); fireEvent(element, "hass-default-panel", { defaultPanel: urlPath });
}; };
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => {
hass.panels[hass.defaultPanel] if (!hass.defaultPanel) {
return hass.panels[DEFAULT_PANEL];
}
return hass.panels[hass.defaultPanel]
? hass.panels[hass.defaultPanel] ? hass.panels[hass.defaultPanel]
: hass.panels[DEFAULT_PANEL]; : hass.panels[DEFAULT_PANEL];
};
export const getPanelNameTranslationKey = (panel: PanelInfo) => { export const getPanelNameTranslationKey = (panel: PanelInfo) => {
if (panel.url_path === "lovelace") { if (panel.url_path === "lovelace") {

View File

@@ -54,9 +54,10 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
const path = curPath(); const path = curPath();
if (["", "/"].includes(path)) { if (["", "/"].includes(path)) {
navigate(`/${getStorageDefaultPanelUrlPath()}${location.search}`, { const defaultPanel = getStorageDefaultPanelUrlPath();
replace: true, if (defaultPanel) {
}); navigate(`/${defaultPanel}${location.search}`, { replace: true });
}
} }
this._route = { this._route = {
prefix: "", prefix: "",

View File

@@ -188,6 +188,10 @@ class PartialPanelResolver extends HassRouterPage {
} }
private async _updateRoutes(oldPanels?: HomeAssistant["panels"]) { private async _updateRoutes(oldPanels?: HomeAssistant["panels"]) {
if (this.hass.defaultPanel) {
await this.hass.loadDefaultPanel();
}
this.routerOptions = this._getRoutes(this.hass.panels); this.routerOptions = this._getRoutes(this.hass.panels);
if ( if (

View File

@@ -8,13 +8,18 @@ import {
subscribeServices, subscribeServices,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { computeStateName } from "../common/entity/compute_state_name";
import { promiseTimeout } from "../common/util/promise-timeout"; import { promiseTimeout } from "../common/util/promise-timeout";
import { subscribeAreaRegistry } from "../data/area_registry"; import { subscribeAreaRegistry } from "../data/area_registry";
import { broadcastConnectionStatus } from "../data/connection-status"; import { broadcastConnectionStatus } from "../data/connection-status";
import { subscribeDeviceRegistry } from "../data/device_registry"; import { subscribeDeviceRegistry } from "../data/device_registry";
import { subscribeFrontendUserData } from "../data/frontend"; import {
fetchFrontendSystemData,
fetchFrontendUserData,
subscribeFrontendSystemData,
subscribeFrontendUserData,
} from "../data/frontend";
import { forwardHaptic } from "../data/haptics"; import { forwardHaptic } from "../data/haptics";
import { DEFAULT_PANEL } from "../data/panel";
import { serviceCallWillDisconnect } from "../data/service"; import { serviceCallWillDisconnect } from "../data/service";
import { import {
DateFormat, DateFormat,
@@ -33,7 +38,7 @@ import { fetchWithAuth } from "../util/fetch-with-auth";
import { getState, storeState } from "../util/ha-pref-storage"; import { getState, storeState } from "../util/ha-pref-storage";
import hassCallApi, { hassCallApiRaw } from "../util/hass-call-api"; import hassCallApi, { hassCallApiRaw } from "../util/hass-call-api";
import type { HassBaseEl } from "./hass-base-mixin"; import type { HassBaseEl } from "./hass-base-mixin";
import { computeStateName } from "../common/entity/compute_state_name"; import { DEFAULT_PANEL } from "../data/panel";
export const connectionMixin = <T extends Constructor<HassBaseEl>>( export const connectionMixin = <T extends Constructor<HassBaseEl>>(
superClass: T superClass: T
@@ -73,7 +78,6 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
}, },
resources: null as any, resources: null as any,
localize: () => "", localize: () => "",
translationMetadata, translationMetadata,
dockedSidebar: "docked", dockedSidebar: "docked",
vibrate: true, vibrate: true,
@@ -204,14 +208,28 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
loadFragmentTranslation: (fragment) => loadFragmentTranslation: (fragment) =>
// @ts-ignore // @ts-ignore
this._loadFragmentTranslations(this.hass?.language, fragment), this._loadFragmentTranslations(this.hass?.language, fragment),
loadDefaultPanel: async () => {
const [user, system] = await Promise.all([
fetchFrontendUserData(conn, "default_panel"),
fetchFrontendSystemData(conn, "default_panel"),
]);
const defaultPanel = user || system;
this._updateHass({
userDefaultPanel: user,
systemDefaultPanel: system,
defaultPanel,
});
storeState(this.hass!);
return defaultPanel;
},
formatEntityState: (stateObj, state) => formatEntityState: (stateObj, state) =>
(state != null ? state : stateObj.state) ?? "", (state != null ? state : stateObj.state) ?? "",
formatEntityAttributeName: (_stateObj, attribute) => attribute, formatEntityAttributeName: (_stateObj, attribute) => attribute,
formatEntityAttributeValue: (stateObj, attribute, value) => formatEntityAttributeValue: (stateObj, attribute, value) =>
value != null ? value : (stateObj.attributes[attribute] ?? ""), value != null ? value : (stateObj.attributes[attribute] ?? ""),
formatEntityName: (stateObj) => computeStateName(stateObj),
...getState(), ...getState(),
...this._pendingHass, ...this._pendingHass,
formatEntityName: (stateObj) => computeStateName(stateObj),
}; };
this.hassConnected(); this.hassConnected();
@@ -289,11 +307,34 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
conn, conn,
"default_panel", "default_panel",
({ value: defaultPanel }) => { ({ value: defaultPanel }) => {
this._updateHass({ defaultPanel: defaultPanel || DEFAULT_PANEL }); this._updateHass({
userDefaultPanel: defaultPanel,
});
// Update default panel taking into account user and system default panel
this._updateHass({
defaultPanel:
this.hass!.userDefaultPanel || this.hass!.systemDefaultPanel,
});
// Store updated default panel in local storage
storeState(this.hass!);
}
);
subscribeFrontendSystemData(
conn,
"default_panel",
({ value: defaultPanel }) => {
this._updateHass({
systemDefaultPanel: defaultPanel,
});
// Update default panel taking into account user and system default panel
this._updateHass({
defaultPanel:
this.hass!.userDefaultPanel || this.hass!.systemDefaultPanel,
});
// Store updated default panel in local storage
storeState(this.hass!); storeState(this.hass!);
} }
); );
clearInterval(this.__backendPingInterval); clearInterval(this.__backendPingInterval);
this.__backendPingInterval = setInterval(() => { this.__backendPingInterval = setInterval(() => {
if (this.hass?.connected) { if (this.hass?.connected) {

View File

@@ -248,7 +248,9 @@ export interface HomeAssistant {
vibrate: boolean; vibrate: boolean;
debugConnection: boolean; debugConnection: boolean;
dockedSidebar: "docked" | "always_hidden" | "auto"; dockedSidebar: "docked" | "always_hidden" | "auto";
defaultPanel: string; defaultPanel?: string | null;
userDefaultPanel?: string | null;
systemDefaultPanel?: string | null;
moreInfoEntityId: string | null; moreInfoEntityId: string | null;
user?: CurrentUser; user?: CurrentUser;
userData?: CoreFrontendUserData | null; userData?: CoreFrontendUserData | null;
@@ -283,6 +285,7 @@ export interface HomeAssistant {
configFlow?: Parameters<typeof getHassTranslations>[4] configFlow?: Parameters<typeof getHassTranslations>[4]
): Promise<LocalizeFunc>; ): Promise<LocalizeFunc>;
loadFragmentTranslation(fragment: string): Promise<LocalizeFunc | undefined>; loadFragmentTranslation(fragment: string): Promise<LocalizeFunc | undefined>;
loadDefaultPanel(): Promise<string | null | undefined>;
formatEntityState(stateObj: HassEntity, state?: string): string; formatEntityState(stateObj: HassEntity, state?: string): string;
formatEntityAttributeValue( formatEntityAttributeValue(
stateObj: HassEntity, stateObj: HassEntity,

View File

@@ -9,7 +9,9 @@ const STORED_STATE = [
"suspendWhenHidden", "suspendWhenHidden",
"enableShortcuts", "enableShortcuts",
"defaultPanel", "defaultPanel",
]; ] as const;
type StoredHomeAssistant = Pick<HomeAssistant, (typeof STORED_STATE)[number]>;
export function storeState(hass: HomeAssistant) { export function storeState(hass: HomeAssistant) {
try { try {
@@ -31,8 +33,8 @@ export function storeState(hass: HomeAssistant) {
} }
} }
export function getState() { export function getState(): Partial<StoredHomeAssistant> {
const state = {}; const state = {} as Partial<StoredHomeAssistant>;
STORED_STATE.forEach((key) => { STORED_STATE.forEach((key) => {
const storageItem = window.localStorage.getItem(key); const storageItem = window.localStorage.getItem(key);