Skip to content
12 changes: 9 additions & 3 deletions cast/src/receiver/layout/hc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,17 @@ export class HcMain extends HassElement {
}

private async _generateLovelaceConfig() {
const { generateLovelaceConfigFromHass } = await import(
"../../../../src/panels/lovelace/common/generate-lovelace-config"
const { generateLovelaceDashboardStrategy } = await import(
"../../../../src/panels/lovelace/strategies/get-strategy"
);
this._handleNewLovelaceConfig(
await generateLovelaceConfigFromHass(this.hass!)
await generateLovelaceDashboardStrategy(
{
hass: this.hass!,
narrow: false,
},
"original-states"
)
);
}

Expand Down
11 changes: 11 additions & 0 deletions src/data/lovelace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export interface LovelacePanelConfig {

export interface LovelaceConfig {
title?: string;
// When specified, we execute strategy and merge into config on top level (no deep merge)
strategy?: {
name: string;
options?: Record<string, unknown>;
};
views: LovelaceViewConfig[];
background?: string;
}
Expand Down Expand Up @@ -77,6 +82,11 @@ export interface LovelaceViewConfig {
index?: number;
title?: string;
type?: string;
// When specified, we execute strategy and merge into view config on top level (no deep merge)
strategy?: {
name: string;
options?: Record<string, unknown>;
};
badges?: Array<string | LovelaceBadgeConfig>;
cards?: LovelaceCardConfig[];
path?: string;
Expand All @@ -94,6 +104,7 @@ export interface LovelaceViewElement extends HTMLElement {
index?: number;
cards?: Array<LovelaceCard | HuiErrorCard>;
badges?: LovelaceBadge[];
isStrategy: boolean;
setConfig(config: LovelaceViewConfig): void;
}

Expand Down
15 changes: 11 additions & 4 deletions src/panels/lovelace/cards/hui-error-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,18 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
return html``;
}

let dumped: string | undefined;

if (this._config.origConfig) {
try {
dumped = safeDump(this._config.origConfig);
} catch (err) {
dumped = `[Error dumping ${this._config.origConfig}]`;
}
}

return html`
${this._config.error}
${this._config.origConfig
? html`<pre>${safeDump(this._config.origConfig)}</pre>`
: ""}
${this._config.error}${dumped ? html`<pre>${dumped}</pre>` : ""}
`;
}

Expand Down
177 changes: 6 additions & 171 deletions src/panels/lovelace/common/generate-lovelace-config.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,16 @@
import {
HassEntities,
HassEntity,
STATE_NOT_RUNNING,
} from "home-assistant-js-websocket";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { DEFAULT_VIEW_ENTITY_ID } from "../../../common/const";
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { extractViews } from "../../../common/entity/extract_views";
import { getViewEntities } from "../../../common/entity/get_view_entities";
import { splitByGroups } from "../../../common/entity/split_by_groups";
import { compare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize";
import { subscribeOne } from "../../../common/util/subscribe-one";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../../data/area_registry";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import { GroupEntity } from "../../../data/group";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity_registry";
import { domainToName } from "../../../data/integration";
import {
LovelaceCardConfig,
LovelaceConfig,
LovelaceViewConfig,
} from "../../../data/lovelace";
import { LovelaceCardConfig, LovelaceViewConfig } from "../../../data/lovelace";
import { SENSOR_DEVICE_CLASS_BATTERY } from "../../../data/sensor";
import { HomeAssistant } from "../../../types";
import {
AlarmPanelCardConfig,
EntitiesCardConfig,
Expand All @@ -57,8 +32,6 @@ const HIDE_DOMAIN = new Set([

const HIDE_PLATFORM = new Set(["mobile_app"]);

let subscribedRegistries = false;

interface SplittedByAreas {
areasWithEntities: Array<[AreaRegistryEntry, HassEntity[]]>;
otherEntities: HassEntities;
Expand Down Expand Up @@ -239,7 +212,7 @@ const computeDefaultViewStates = (
return states;
};

const generateViewConfig = (
export const generateViewConfig = (
localize: LocalizeFunc,
path: string,
title: string | undefined,
Expand Down Expand Up @@ -373,141 +346,3 @@ export const generateDefaultViewConfig = (

return config;
};

export const generateLovelaceConfigFromData = async (
hass: HomeAssistant,
areaEntries: AreaRegistryEntry[],
deviceEntries: DeviceRegistryEntry[],
entityEntries: EntityRegistryEntry[],
entities: HassEntities,
localize: LocalizeFunc
): Promise<LovelaceConfig> => {
if (hass.config.safe_mode) {
return {
title: hass.config.location_name,
views: [
{
cards: [{ type: "safe-mode" }],
},
],
};
}

const viewEntities = extractViews(entities);

const views = viewEntities.map((viewEntity: GroupEntity) => {
const states = getViewEntities(entities, viewEntity);

// In the case of a normal view, we use group order as specified in view
const groupOrders = {};
Object.keys(states).forEach((entityId, idx) => {
groupOrders[entityId] = idx;
});

return generateViewConfig(
localize,
computeObjectId(viewEntity.entity_id),
computeStateName(viewEntity),
viewEntity.attributes.icon,
states,
groupOrders
);
});

let title = hass.config.location_name;

// User can override default view. If they didn't, we will add one
// that contains all entities.
if (
viewEntities.length === 0 ||
viewEntities[0].entity_id !== DEFAULT_VIEW_ENTITY_ID
) {
views.unshift(
generateDefaultViewConfig(
areaEntries,
deviceEntries,
entityEntries,
entities,
localize
)
);

// Add map of geo locations to default view if loaded
if (isComponentLoaded(hass, "geo_location")) {
if (views[0] && views[0].cards) {
views[0].cards.push({
type: "map",
geo_location_sources: ["all"],
});
}
}

// Make sure we don't have Home as title and first tab.
if (views.length > 1 && title === "Home") {
title = "Home Assistant";
}
}

// User has no entities
if (views.length === 1 && views[0].cards!.length === 0) {
views[0].cards!.push({
type: "empty-state",
});
}

return {
title,
views,
};
};

export const generateLovelaceConfigFromHass = async (
hass: HomeAssistant,
localize?: LocalizeFunc
): Promise<LovelaceConfig> => {
if (hass.config.state === STATE_NOT_RUNNING) {
return {
title: hass.config.location_name,
views: [
{
cards: [{ type: "starting" }],
},
],
};
}

if (hass.config.safe_mode) {
return {
title: hass.config.location_name,
views: [
{
cards: [{ type: "safe-mode" }],
},
],
};
}

// We want to keep the registry subscriptions alive after generating the UI
// so that we don't serve up stale data after changing areas.
if (!subscribedRegistries) {
subscribedRegistries = true;
subscribeAreaRegistry(hass.connection, () => undefined);
subscribeDeviceRegistry(hass.connection, () => undefined);
subscribeEntityRegistry(hass.connection, () => undefined);
}

const [areaEntries, deviceEntries, entityEntries] = await Promise.all([
subscribeOne(hass.connection, subscribeAreaRegistry),
subscribeOne(hass.connection, subscribeDeviceRegistry),
subscribeOne(hass.connection, subscribeEntityRegistry),
]);

return generateLovelaceConfigFromData(
hass,
areaEntries,
deviceEntries,
entityEntries,
hass.states,
localize || hass.localize
);
};
3 changes: 2 additions & 1 deletion src/panels/lovelace/create-element/create-view-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
LovelaceViewConfig,
LovelaceViewElement,
} from "../../../data/lovelace";
import { HuiErrorCard } from "../cards/hui-error-card";
import "../views/hui-masonry-view";
import { createLovelaceElement } from "./create-element-base";

Expand All @@ -13,7 +14,7 @@ const LAZY_LOAD_LAYOUTS = {

export const createViewElement = (
config: LovelaceViewConfig
): LovelaceViewElement => {
): LovelaceViewElement | HuiErrorCard => {
return createLovelaceElement(
"view",
config,
Expand Down
40 changes: 34 additions & 6 deletions src/panels/lovelace/ha-panel-lovelace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ import "../../layouts/hass-error-screen";
import "../../layouts/hass-loading-screen";
import { HomeAssistant, PanelInfo, Route } from "../../types";
import { showToast } from "../../util/toast";
import { generateLovelaceConfigFromHass } from "./common/generate-lovelace-config";
import { loadLovelaceResources } from "./common/load-resources";
import { showSaveDialog } from "./editor/show-save-config-dialog";
import "./hui-root";
import { generateLovelaceDashboardStrategy } from "./strategies/get-strategy";
import { Lovelace } from "./types";

(window as any).loadCardHelpers = () => import("./custom-card-helpers");

const DEFAULT_STRATEGY = "original-states";

interface LovelacePanelConfig {
mode: "yaml" | "storage";
}
Expand Down Expand Up @@ -153,7 +155,13 @@ class LovelacePanel extends LitElement {
}

private async _regenerateConfig() {
const conf = await generateLovelaceConfigFromHass(this.hass!);
const conf = await generateLovelaceDashboardStrategy(
{
hass: this.hass!,
narrow: this.narrow,
},
DEFAULT_STRATEGY
);
this._setLovelaceConfig(conf, "generated");
this._state = "loaded";
}
Expand Down Expand Up @@ -237,6 +245,15 @@ class LovelacePanel extends LitElement {

try {
conf = await confProm!;

// If strategy defined, apply it here.
if (conf.strategy) {
conf = await generateLovelaceDashboardStrategy({
config: conf,
hass: this.hass!,
narrow: this.narrow,
});
}
} catch (err) {
if (err.code !== "config_not_found") {
// eslint-disable-next-line
Expand All @@ -245,8 +262,13 @@ class LovelacePanel extends LitElement {
this._errorMsg = err.message;
return;
}
const localize = await this.hass!.loadBackendTranslation("title");
conf = await generateLovelaceConfigFromHass(this.hass!, localize);
conf = await generateLovelaceDashboardStrategy(
{
hass: this.hass!,
narrow: this.narrow,
},
DEFAULT_STRATEGY
);
confMode = "generated";
} finally {
// Ignore updates for another 2 seconds.
Expand Down Expand Up @@ -329,9 +351,15 @@ class LovelacePanel extends LitElement {
const { config: previousConfig, mode: previousMode } = this.lovelace!;
try {
// Optimistic update
const localize = await this.hass!.loadBackendTranslation("title");
const generatedConf = await generateLovelaceDashboardStrategy(
{
hass: this.hass!,
narrow: this.narrow,
},
DEFAULT_STRATEGY
);
this._updateLovelace({
config: await generateLovelaceConfigFromHass(this.hass!, localize),
config: generatedConf,
mode: "generated",
editMode: false,
});
Expand Down
Loading