Compare commits

...
9 changed files with 190 additions and 92 deletions
+2 -2
View File
@@ -27,6 +27,7 @@ import type {
LovelaceConfig,
LovelaceDashboardStrategyConfig,
} from "../../../../src/data/lovelace/config/types";
import { normalizeLovelaceConfig } from "../../../../src/data/lovelace/config/normalize";
import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types";
import { fetchResources } from "../../../../src/data/lovelace/resource";
import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources";
@@ -34,7 +35,6 @@ import { HassElement } from "../../../../src/state/hass-element";
import { castContext } from "../cast_context";
import "./hc-launch-screen";
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
import { checkLovelaceConfig } from "../../../../src/panels/lovelace/common/check-lovelace-config";
const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = {
strategy: {
@@ -359,7 +359,7 @@ export class HcMain extends HassElement {
this._urlPath || "lovelace"
);
castContext.setApplicationState(title || "");
this._lovelaceConfig = checkLovelaceConfig(
this._lovelaceConfig = normalizeLovelaceConfig(
lovelaceConfig
) as LovelaceConfig;
}
+76
View File
@@ -0,0 +1,76 @@
import type { LovelaceBadgeConfig } from "./badge";
import { ensureBadgeConfig } from "./badge";
import type { LovelaceSectionRawConfig } from "./section";
import { isStrategySection } from "./section";
import type { LovelaceRawConfig } from "./types";
import { isStrategyDashboard } from "./types";
import type { LovelaceViewConfig } from "./view";
import { isStrategyView } from "./view";
export const normalizeLovelaceConfig = (
config: LovelaceRawConfig
): LovelaceRawConfig => {
if (isStrategyDashboard(config)) {
return config;
}
const updatedConfig = { ...config };
if (updatedConfig.views) {
updatedConfig.views = updatedConfig.views
.filter(Boolean)
.map((view) => (isStrategyView(view) ? view : normalizeViewConfig(view)));
}
return updatedConfig;
};
export const normalizeViewConfig = (
view: LovelaceViewConfig
): LovelaceViewConfig => {
const updatedView = { ...view };
// Remove empty badges and expand the entity id shorthand
if (updatedView.badges) {
const badges = updatedView.badges as (
Partial<LovelaceBadgeConfig> | string
)[];
updatedView.badges = badges.filter(Boolean).map(ensureBadgeConfig);
}
if (updatedView.cards) {
updatedView.cards = updatedView.cards.filter(Boolean);
}
// Migrate sections
if (updatedView.sections) {
updatedView.sections = updatedView.sections
.filter(Boolean)
.map(normalizeSectionConfig);
}
return updatedView;
};
export const normalizeSectionConfig = (
section: LovelaceSectionRawConfig
): LovelaceSectionRawConfig => {
const updatedSection = { ...section };
// Move title to a heading card
if (section.title) {
// Only add card if it's not a strategy section
if (!isStrategySection(updatedSection)) {
const card = { type: "heading", heading: updatedSection.title };
updatedSection.cards = [card, ...(updatedSection.cards || [])];
}
delete updatedSection.title;
}
if (!isStrategySection(updatedSection) && updatedSection.cards) {
updatedSection.cards = updatedSection.cards.filter(Boolean);
}
return updatedSection;
};
+1 -1
View File
@@ -75,7 +75,7 @@ export interface LovelaceBaseViewConfig {
export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
type?: string;
badges?: (string | Partial<LovelaceBadgeConfig>)[]; // Badge can be just an entity_id or without type
badges?: LovelaceBadgeConfig[];
cards?: LovelaceCardConfig[];
sections?: LovelaceSectionRawConfig[];
header?: LovelaceViewHeaderConfig;
@@ -1,62 +0,0 @@
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
import { isStrategySection } from "../../../data/lovelace/config/section";
import type { LovelaceRawConfig } from "../../../data/lovelace/config/types";
import { isStrategyDashboard } from "../../../data/lovelace/config/types";
import type { LovelaceViewRawConfig } from "../../../data/lovelace/config/view";
import { isStrategyView } from "../../../data/lovelace/config/view";
export const checkLovelaceConfig = (
config: LovelaceRawConfig
): LovelaceRawConfig => {
if (isStrategyDashboard(config)) {
return config;
}
const updatedConfig = { ...config };
if (updatedConfig.views) {
updatedConfig.views = updatedConfig.views.map(checkViewConfig);
}
return updatedConfig;
};
export const checkViewConfig = (
view: LovelaceViewRawConfig
): LovelaceViewRawConfig => {
if (isStrategyView(view)) {
return view;
}
const updatedView = { ...view };
// Remove empty badges
if (updatedView.badges && !updatedView.badges.every(Boolean)) {
updatedView.badges = updatedView.badges!.filter(Boolean);
}
// Migrate sections
if (updatedView.sections) {
updatedView.sections = updatedView.sections!.map(checkSectionConfig);
}
return updatedView;
};
export const checkSectionConfig = (
section: LovelaceSectionRawConfig
): LovelaceSectionRawConfig => {
const updatedSection = { ...section };
// Move title to a heading card
if (section.title) {
// Only add card if it's not a strategy section
if (!isStrategySection(updatedSection)) {
const card = { type: "heading", heading: updatedSection.title };
updatedSection.cards = [card, ...(updatedSection.cards || [])];
}
delete updatedSection.title;
}
return updatedSection;
};
@@ -21,10 +21,7 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import {
ensureBadgeConfig,
type LovelaceBadgeConfig,
} from "../../../data/lovelace/config/badge";
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
import { haStyle } from "../../../resources/styles";
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
import type { LovelacePath } from "../editor/lovelace-path";
@@ -66,12 +63,7 @@ export class HuiBadgeEditMode extends LitElement {
protected _clipboard?: LovelaceBadgeConfig;
private get _badgeConfig() {
return ensureBadgeConfig(
getAtPath<Partial<LovelaceBadgeConfig> | string>(
this.lovelace.config,
this.path
)!
);
return getAtPath<LovelaceBadgeConfig>(this.lovelace.config, this.path)!;
}
private _touchStarted = false;
+9 -7
View File
@@ -15,6 +15,7 @@ import { debounce } from "../../common/util/debounce";
import "../../components/ha-button";
import { domainToName } from "../../data/integration";
import { subscribeLovelaceUpdates } from "../../data/lovelace";
import { normalizeLovelaceConfig } from "../../data/lovelace/config/normalize";
import type {
LovelaceConfig,
LovelaceRawConfig,
@@ -31,7 +32,6 @@ import "../../layouts/hass-loading-screen";
import type { ShowToastParams } from "../../managers/notification-manager";
import type { HomeAssistant, PanelInfo, Route } from "../../types";
import { showToast } from "../../util/toast";
import { checkLovelaceConfig } from "./common/check-lovelace-config";
import { loadLovelaceResources } from "./common/load-resources";
import { showSaveDialog } from "./editor/show-save-config-dialog";
import "./hui-root";
@@ -379,9 +379,9 @@ export class LovelacePanel extends LitElement {
this._setLovelaceConfig(conf, rawConf, confMode);
}
private _checkLovelaceConfig(config: LovelaceRawConfig) {
const checkedConfig = checkLovelaceConfig(config);
return deepFreeze(checkedConfig);
private _normalizeLovelaceConfig(config: LovelaceRawConfig) {
const normalizedConfig = normalizeLovelaceConfig(config);
return deepFreeze(normalizedConfig);
}
private _setLovelaceConfig(
@@ -389,7 +389,7 @@ export class LovelacePanel extends LitElement {
rawConfig: LovelaceRawConfig,
mode: Lovelace["mode"]
) {
config = this._checkLovelaceConfig(config);
config = this._normalizeLovelaceConfig(config);
const urlPath = this.urlPath;
this.lovelace = {
config,
@@ -436,11 +436,13 @@ export class LovelacePanel extends LitElement {
rawConfig: previousRawConfig,
mode: previousMode,
} = this.lovelace!;
newConfig = this._checkLovelaceConfig(newConfig);
newConfig = this._normalizeLovelaceConfig(newConfig);
let conf: LovelaceConfig;
// If strategy defined, apply it here.
if (isStrategyDashboard(newConfig)) {
conf = await generateLovelaceDashboardStrategy(newConfig, this.hass!);
conf = this._normalizeLovelaceConfig(
await generateLovelaceDashboardStrategy(newConfig, this.hass!)
);
} else {
conf = newConfig;
}
+6 -9
View File
@@ -11,13 +11,13 @@ import { deepEqual } from "../../../common/util/deep-equal";
import "../../../components/ha-svg-icon";
import type { LovelaceViewElement } from "../../../data/lovelace";
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
import { ensureBadgeConfig } from "../../../data/lovelace/config/badge";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type {
LovelaceViewConfig,
LovelaceViewRawConfig,
} from "../../../data/lovelace/config/view";
import { normalizeViewConfig } from "../../../data/lovelace/config/normalize";
import { isStrategyView } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import {
@@ -232,7 +232,7 @@ export class HUIView extends ReactiveElement {
"ll-edit-badge",
(ev: HASSDomEvent<HASSDomEvents["ll-edit-badge"]>) => {
const path = ev.detail.path;
const badge = getAtPath<Partial<LovelaceBadgeConfig> | string>(
const badge = getAtPath<LovelaceBadgeConfig>(
this.lovelace.config,
path
);
@@ -249,7 +249,7 @@ export class HUIView extends ReactiveElement {
);
await this.lovelace.saveConfig(newConfig);
},
badgeConfig: ensureBadgeConfig(badge),
badgeConfig: badge,
});
}
);
@@ -435,10 +435,11 @@ export class HUIView extends ReactiveElement {
config: LovelaceViewRawConfig
): Promise<LovelaceViewConfig> {
if (isStrategyView(config)) {
const generatedConfig = await generateLovelaceViewStrategy(
const strategyConfig = await generateLovelaceViewStrategy(
config,
this.hass!
);
const generatedConfig = normalizeViewConfig(strategyConfig);
return {
...generatedConfig,
type: getViewType(generatedConfig),
@@ -525,11 +526,7 @@ export class HUIView extends ReactiveElement {
return;
}
this._badges = config.badges.map((badge) => {
const badgeConfig = ensureBadgeConfig(badge);
const element = this.createBadgeElement(badgeConfig);
return element;
});
this._badges = config.badges.map((badge) => this.createBadgeElement(badge));
}
private _createCards(config: LovelaceViewConfig): void {
@@ -0,0 +1,90 @@
import { describe, expect, it } from "vitest";
import {
normalizeLovelaceConfig,
normalizeViewConfig,
} from "../../../../src/data/lovelace/config/normalize";
import type { LovelaceRawConfig } from "../../../../src/data/lovelace/config/types";
import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
const rawConfig = (config: unknown) => config as LovelaceRawConfig;
describe("normalizeLovelaceConfig", () => {
it("expands badge shorthands into entity badges", () => {
const config = rawConfig({
views: [
{
badges: [
"sensor.temperature",
{ entity: "sensor.humidity" },
{ type: "custom:my-badge" },
],
},
],
});
expect(normalizeLovelaceConfig(config)).toEqual({
views: [
{
badges: [
{ type: "entity", entity: "sensor.temperature", show_name: true },
{ type: "entity", entity: "sensor.humidity" },
{ type: "custom:my-badge" },
],
},
],
});
});
it("removes empty entries at every level", () => {
const config = rawConfig({
views: [
null,
{
badges: [null, "sensor.temperature"],
cards: [null, { type: "tile" }],
sections: [null, { type: "grid", cards: [{ type: "tile" }, null] }],
},
],
});
expect(normalizeLovelaceConfig(config)).toEqual({
views: [
{
badges: [
{ type: "entity", entity: "sensor.temperature", show_name: true },
],
cards: [{ type: "tile" }],
sections: [{ type: "grid", cards: [{ type: "tile" }] }],
},
],
});
});
it("leaves strategies untouched", () => {
const dashboard = rawConfig({ strategy: { type: "original-states" } });
expect(normalizeLovelaceConfig(dashboard)).toBe(dashboard);
const view = { strategy: { type: "areas" } };
const section = { strategy: { type: "common-controls" } };
const config = rawConfig({ views: [view, { sections: [section] }] });
expect(normalizeLovelaceConfig(config)).toEqual({
views: [view, { sections: [section] }],
});
});
});
describe("normalizeViewConfig", () => {
it("expands badge shorthands in a generated view", () => {
const view = {
badges: ["sensor.temperature"],
} as unknown as LovelaceViewConfig;
expect(normalizeViewConfig(view)).toEqual({
badges: [
{ type: "entity", entity: "sensor.temperature", show_name: true },
],
});
});
});
@@ -25,7 +25,10 @@ const createConfig = (): LovelaceConfig => ({
views: [
{
title: "Home",
badges: ["sensor.badge0", { type: "entity", entity: "sensor.badge1" }],
badges: [
{ type: "entity", entity: "sensor.badge0" },
{ type: "entity", entity: "sensor.badge1" },
],
cards: [{ type: "v0-c0" }, { type: "v0-c1" }],
},
{