mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 11:16:35 +00:00
Sketch out strategies (#8959)
Co-authored-by: Zack Arnett <arnett.zackary@gmail.com>
This commit is contained in:
parent
b599417a37
commit
63e10314bd
@ -35,6 +35,7 @@ class HcLovelace extends LitElement {
|
|||||||
}
|
}
|
||||||
const lovelace: Lovelace = {
|
const lovelace: Lovelace = {
|
||||||
config: this.lovelaceConfig,
|
config: this.lovelaceConfig,
|
||||||
|
rawConfig: this.lovelaceConfig,
|
||||||
editMode: false,
|
editMode: false,
|
||||||
urlPath: this.urlPath!,
|
urlPath: this.urlPath!,
|
||||||
enableFullEditMode: () => undefined,
|
enableFullEditMode: () => undefined,
|
||||||
|
@ -221,11 +221,17 @@ export class HcMain extends HassElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _generateLovelaceConfig() {
|
private async _generateLovelaceConfig() {
|
||||||
const { generateLovelaceConfigFromHass } = await import(
|
const { generateLovelaceDashboardStrategy } = await import(
|
||||||
"../../../../src/panels/lovelace/common/generate-lovelace-config"
|
"../../../../src/panels/lovelace/strategies/get-strategy"
|
||||||
);
|
);
|
||||||
this._handleNewLovelaceConfig(
|
this._handleNewLovelaceConfig(
|
||||||
await generateLovelaceConfigFromHass(this.hass!)
|
await generateLovelaceDashboardStrategy(
|
||||||
|
{
|
||||||
|
hass: this.hass!,
|
||||||
|
narrow: false,
|
||||||
|
},
|
||||||
|
"original-states"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,10 @@ export interface LovelacePanelConfig {
|
|||||||
|
|
||||||
export interface LovelaceConfig {
|
export interface LovelaceConfig {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
strategy?: {
|
||||||
|
name: string;
|
||||||
|
options?: Record<string, unknown>;
|
||||||
|
};
|
||||||
views: LovelaceViewConfig[];
|
views: LovelaceViewConfig[];
|
||||||
background?: string;
|
background?: string;
|
||||||
}
|
}
|
||||||
@ -77,6 +81,10 @@ export interface LovelaceViewConfig {
|
|||||||
index?: number;
|
index?: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
strategy?: {
|
||||||
|
name: string;
|
||||||
|
options?: Record<string, unknown>;
|
||||||
|
};
|
||||||
badges?: Array<string | LovelaceBadgeConfig>;
|
badges?: Array<string | LovelaceBadgeConfig>;
|
||||||
cards?: LovelaceCardConfig[];
|
cards?: LovelaceCardConfig[];
|
||||||
path?: string;
|
path?: string;
|
||||||
@ -94,6 +102,7 @@ export interface LovelaceViewElement extends HTMLElement {
|
|||||||
index?: number;
|
index?: number;
|
||||||
cards?: Array<LovelaceCard | HuiErrorCard>;
|
cards?: Array<LovelaceCard | HuiErrorCard>;
|
||||||
badges?: LovelaceBadge[];
|
badges?: LovelaceBadge[];
|
||||||
|
isStrategy: boolean;
|
||||||
setConfig(config: LovelaceViewConfig): void;
|
setConfig(config: LovelaceViewConfig): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,11 +31,18 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
|
|||||||
return html``;
|
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`
|
return html`
|
||||||
${this._config.error}
|
${this._config.error}${dumped ? html`<pre>${dumped}</pre>` : ""}
|
||||||
${this._config.origConfig
|
|
||||||
? html`<pre>${safeDump(this._config.origConfig)}</pre>`
|
|
||||||
: ""}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,41 +1,16 @@
|
|||||||
import {
|
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||||
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 { computeDomain } from "../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
|
||||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
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 { splitByGroups } from "../../../common/entity/split_by_groups";
|
||||||
import { compare } from "../../../common/string/compare";
|
import { compare } from "../../../common/string/compare";
|
||||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||||
import { subscribeOne } from "../../../common/util/subscribe-one";
|
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||||
import {
|
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||||
AreaRegistryEntry,
|
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||||
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 { domainToName } from "../../../data/integration";
|
import { domainToName } from "../../../data/integration";
|
||||||
import {
|
import { LovelaceCardConfig, LovelaceViewConfig } from "../../../data/lovelace";
|
||||||
LovelaceCardConfig,
|
|
||||||
LovelaceConfig,
|
|
||||||
LovelaceViewConfig,
|
|
||||||
} from "../../../data/lovelace";
|
|
||||||
import { SENSOR_DEVICE_CLASS_BATTERY } from "../../../data/sensor";
|
import { SENSOR_DEVICE_CLASS_BATTERY } from "../../../data/sensor";
|
||||||
import { HomeAssistant } from "../../../types";
|
|
||||||
import {
|
import {
|
||||||
AlarmPanelCardConfig,
|
AlarmPanelCardConfig,
|
||||||
EntitiesCardConfig,
|
EntitiesCardConfig,
|
||||||
@ -57,8 +32,6 @@ const HIDE_DOMAIN = new Set([
|
|||||||
|
|
||||||
const HIDE_PLATFORM = new Set(["mobile_app"]);
|
const HIDE_PLATFORM = new Set(["mobile_app"]);
|
||||||
|
|
||||||
let subscribedRegistries = false;
|
|
||||||
|
|
||||||
interface SplittedByAreas {
|
interface SplittedByAreas {
|
||||||
areasWithEntities: Array<[AreaRegistryEntry, HassEntity[]]>;
|
areasWithEntities: Array<[AreaRegistryEntry, HassEntity[]]>;
|
||||||
otherEntities: HassEntities;
|
otherEntities: HassEntities;
|
||||||
@ -239,7 +212,7 @@ const computeDefaultViewStates = (
|
|||||||
return states;
|
return states;
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateViewConfig = (
|
export const generateViewConfig = (
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
path: string,
|
path: string,
|
||||||
title: string | undefined,
|
title: string | undefined,
|
||||||
@ -373,141 +346,3 @@ export const generateDefaultViewConfig = (
|
|||||||
|
|
||||||
return config;
|
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
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -2,6 +2,7 @@ import {
|
|||||||
LovelaceViewConfig,
|
LovelaceViewConfig,
|
||||||
LovelaceViewElement,
|
LovelaceViewElement,
|
||||||
} from "../../../data/lovelace";
|
} from "../../../data/lovelace";
|
||||||
|
import { HuiErrorCard } from "../cards/hui-error-card";
|
||||||
import "../views/hui-masonry-view";
|
import "../views/hui-masonry-view";
|
||||||
import { createLovelaceElement } from "./create-element-base";
|
import { createLovelaceElement } from "./create-element-base";
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ const LAZY_LOAD_LAYOUTS = {
|
|||||||
|
|
||||||
export const createViewElement = (
|
export const createViewElement = (
|
||||||
config: LovelaceViewConfig
|
config: LovelaceViewConfig
|
||||||
): LovelaceViewElement => {
|
): LovelaceViewElement | HuiErrorCard => {
|
||||||
return createLovelaceElement(
|
return createLovelaceElement(
|
||||||
"view",
|
"view",
|
||||||
config,
|
config,
|
||||||
|
@ -19,13 +19,15 @@ import "../../../components/ha-formfield";
|
|||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import "../../../components/ha-switch";
|
import "../../../components/ha-switch";
|
||||||
import "../../../components/ha-yaml-editor";
|
import "../../../components/ha-yaml-editor";
|
||||||
|
import type { LovelaceConfig } from "../../../data/lovelace";
|
||||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||||
import { haStyleDialog } from "../../../resources/styles";
|
import { haStyleDialog } from "../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { documentationUrl } from "../../../util/documentation-url";
|
import { documentationUrl } from "../../../util/documentation-url";
|
||||||
|
import { expandLovelaceConfigStrategies } from "../strategies/get-strategy";
|
||||||
import type { SaveDialogParams } from "./show-save-config-dialog";
|
import type { SaveDialogParams } from "./show-save-config-dialog";
|
||||||
|
|
||||||
const EMPTY_CONFIG = { views: [] };
|
const EMPTY_CONFIG: LovelaceConfig = { views: [{ title: "Home" }] };
|
||||||
|
|
||||||
@customElement("hui-dialog-save-config")
|
@customElement("hui-dialog-save-config")
|
||||||
export class HuiSaveConfig extends LitElement implements HassDialog {
|
export class HuiSaveConfig extends LitElement implements HassDialog {
|
||||||
@ -125,14 +127,17 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
|
|||||||
</div>
|
</div>
|
||||||
${this._params.mode === "storage"
|
${this._params.mode === "storage"
|
||||||
? html`
|
? html`
|
||||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}
|
<mwc-button
|
||||||
>${this.hass!.localize(
|
slot="primaryAction"
|
||||||
"ui.common.cancel"
|
.label=${this.hass!.localize("ui.common.cancel")}
|
||||||
)}
|
@click=${this.closeDialog}
|
||||||
</mwc-button>
|
></mwc-button>
|
||||||
<mwc-button
|
<mwc-button
|
||||||
slot="primaryAction"
|
slot="primaryAction"
|
||||||
?disabled=${this._saving}
|
?disabled=${this._saving}
|
||||||
|
aria-label=${this.hass!.localize(
|
||||||
|
"ui.panel.lovelace.editor.save_config.save"
|
||||||
|
)}
|
||||||
@click=${this._saveConfig}
|
@click=${this._saveConfig}
|
||||||
>
|
>
|
||||||
${this._saving
|
${this._saving
|
||||||
@ -148,11 +153,13 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
|
|||||||
</mwc-button>
|
</mwc-button>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}
|
<mwc-button
|
||||||
>${this.hass!.localize(
|
slot="primaryAction"
|
||||||
|
.label=${this.hass!.localize(
|
||||||
"ui.panel.lovelace.editor.save_config.close"
|
"ui.panel.lovelace.editor.save_config.close"
|
||||||
)}
|
)}
|
||||||
</mwc-button>
|
@click=${this.closeDialog}
|
||||||
|
></mwc-button>
|
||||||
`}
|
`}
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
`;
|
`;
|
||||||
@ -177,7 +184,13 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
|
|||||||
try {
|
try {
|
||||||
const lovelace = this._params!.lovelace;
|
const lovelace = this._params!.lovelace;
|
||||||
await lovelace.saveConfig(
|
await lovelace.saveConfig(
|
||||||
this._emptyConfig ? EMPTY_CONFIG : lovelace.config
|
this._emptyConfig
|
||||||
|
? EMPTY_CONFIG
|
||||||
|
: await expandLovelaceConfigStrategies({
|
||||||
|
config: lovelace.config,
|
||||||
|
hass: this.hass!,
|
||||||
|
narrow: this._params!.narrow,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
lovelace.setEditMode(true);
|
lovelace.setEditMode(true);
|
||||||
this._saving = false;
|
this._saving = false;
|
||||||
|
@ -14,6 +14,7 @@ const dialogTag = "hui-dialog-save-config";
|
|||||||
export interface SaveDialogParams {
|
export interface SaveDialogParams {
|
||||||
lovelace: Lovelace;
|
lovelace: Lovelace;
|
||||||
mode: "yaml" | "storage";
|
mode: "yaml" | "storage";
|
||||||
|
narrow: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let registeredDialog = false;
|
let registeredDialog = false;
|
||||||
|
@ -7,6 +7,11 @@ import {
|
|||||||
property,
|
property,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
|
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||||
|
import {
|
||||||
|
addSearchParam,
|
||||||
|
removeSearchParam,
|
||||||
|
} from "../../common/url/search-params";
|
||||||
import { domainToName } from "../../data/integration";
|
import { domainToName } from "../../data/integration";
|
||||||
import {
|
import {
|
||||||
deleteConfig,
|
deleteConfig,
|
||||||
@ -21,14 +26,16 @@ import "../../layouts/hass-error-screen";
|
|||||||
import "../../layouts/hass-loading-screen";
|
import "../../layouts/hass-loading-screen";
|
||||||
import { HomeAssistant, PanelInfo, Route } from "../../types";
|
import { HomeAssistant, PanelInfo, Route } from "../../types";
|
||||||
import { showToast } from "../../util/toast";
|
import { showToast } from "../../util/toast";
|
||||||
import { generateLovelaceConfigFromHass } from "./common/generate-lovelace-config";
|
|
||||||
import { loadLovelaceResources } from "./common/load-resources";
|
import { loadLovelaceResources } from "./common/load-resources";
|
||||||
import { showSaveDialog } from "./editor/show-save-config-dialog";
|
import { showSaveDialog } from "./editor/show-save-config-dialog";
|
||||||
import "./hui-root";
|
import "./hui-root";
|
||||||
|
import { generateLovelaceDashboardStrategy } from "./strategies/get-strategy";
|
||||||
import { Lovelace } from "./types";
|
import { Lovelace } from "./types";
|
||||||
|
|
||||||
(window as any).loadCardHelpers = () => import("./custom-card-helpers");
|
(window as any).loadCardHelpers = () => import("./custom-card-helpers");
|
||||||
|
|
||||||
|
const DEFAULT_STRATEGY = "original-states";
|
||||||
|
|
||||||
interface LovelacePanelConfig {
|
interface LovelacePanelConfig {
|
||||||
mode: "yaml" | "storage";
|
mode: "yaml" | "storage";
|
||||||
}
|
}
|
||||||
@ -71,7 +78,11 @@ class LovelacePanel extends LitElement {
|
|||||||
this.lovelace.locale !== this.hass.locale
|
this.lovelace.locale !== this.hass.locale
|
||||||
) {
|
) {
|
||||||
// language has been changed, rebuild UI
|
// language has been changed, rebuild UI
|
||||||
this._setLovelaceConfig(this.lovelace.config, this.lovelace.mode);
|
this._setLovelaceConfig(
|
||||||
|
this.lovelace.config,
|
||||||
|
this.lovelace.rawConfig,
|
||||||
|
this.lovelace.mode
|
||||||
|
);
|
||||||
} else if (this.lovelace && this.lovelace.mode === "generated") {
|
} else if (this.lovelace && this.lovelace.mode === "generated") {
|
||||||
// When lovelace is generated, we re-generate each time a user goes
|
// When lovelace is generated, we re-generate each time a user goes
|
||||||
// to the states panel to make sure new entities are shown.
|
// to the states panel to make sure new entities are shown.
|
||||||
@ -139,7 +150,9 @@ class LovelacePanel extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated() {
|
protected firstUpdated(changedProps) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
|
||||||
this._fetchConfig(false);
|
this._fetchConfig(false);
|
||||||
if (!this._unsubUpdates) {
|
if (!this._unsubUpdates) {
|
||||||
this._subscribeUpdates();
|
this._subscribeUpdates();
|
||||||
@ -153,8 +166,14 @@ class LovelacePanel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _regenerateConfig() {
|
private async _regenerateConfig() {
|
||||||
const conf = await generateLovelaceConfigFromHass(this.hass!);
|
const conf = await generateLovelaceDashboardStrategy(
|
||||||
this._setLovelaceConfig(conf, "generated");
|
{
|
||||||
|
hass: this.hass!,
|
||||||
|
narrow: this.narrow,
|
||||||
|
},
|
||||||
|
DEFAULT_STRATEGY
|
||||||
|
);
|
||||||
|
this._setLovelaceConfig(conf, undefined, "generated");
|
||||||
this._state = "loaded";
|
this._state = "loaded";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,6 +221,7 @@ class LovelacePanel extends LitElement {
|
|||||||
|
|
||||||
private async _fetchConfig(forceDiskRefresh: boolean) {
|
private async _fetchConfig(forceDiskRefresh: boolean) {
|
||||||
let conf: LovelaceConfig;
|
let conf: LovelaceConfig;
|
||||||
|
let rawConf: LovelaceConfig | undefined;
|
||||||
let confMode: Lovelace["mode"] = this.panel!.config.mode;
|
let confMode: Lovelace["mode"] = this.panel!.config.mode;
|
||||||
let confProm: Promise<LovelaceConfig> | undefined;
|
let confProm: Promise<LovelaceConfig> | undefined;
|
||||||
const llWindow = window as WindowWithLovelaceProm;
|
const llWindow = window as WindowWithLovelaceProm;
|
||||||
@ -236,7 +256,18 @@ class LovelacePanel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
conf = await confProm!;
|
rawConf = await confProm!;
|
||||||
|
|
||||||
|
// If strategy defined, apply it here.
|
||||||
|
if (rawConf.strategy) {
|
||||||
|
conf = await generateLovelaceDashboardStrategy({
|
||||||
|
config: rawConf,
|
||||||
|
hass: this.hass!,
|
||||||
|
narrow: this.narrow,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
conf = rawConf;
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code !== "config_not_found") {
|
if (err.code !== "config_not_found") {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@ -245,8 +276,13 @@ class LovelacePanel extends LitElement {
|
|||||||
this._errorMsg = err.message;
|
this._errorMsg = err.message;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const localize = await this.hass!.loadBackendTranslation("title");
|
conf = await generateLovelaceDashboardStrategy(
|
||||||
conf = await generateLovelaceConfigFromHass(this.hass!, localize);
|
{
|
||||||
|
hass: this.hass!,
|
||||||
|
narrow: this.narrow,
|
||||||
|
},
|
||||||
|
DEFAULT_STRATEGY
|
||||||
|
);
|
||||||
confMode = "generated";
|
confMode = "generated";
|
||||||
} finally {
|
} finally {
|
||||||
// Ignore updates for another 2 seconds.
|
// Ignore updates for another 2 seconds.
|
||||||
@ -258,7 +294,7 @@ class LovelacePanel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._state = this._state === "yaml-editor" ? this._state : "loaded";
|
this._state = this._state === "yaml-editor" ? this._state : "loaded";
|
||||||
this._setLovelaceConfig(conf, confMode);
|
this._setLovelaceConfig(conf, rawConf, confMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _checkLovelaceConfig(config: LovelaceConfig) {
|
private _checkLovelaceConfig(config: LovelaceConfig) {
|
||||||
@ -277,11 +313,16 @@ class LovelacePanel extends LitElement {
|
|||||||
return checkedConfig ? deepFreeze(checkedConfig) : config;
|
return checkedConfig ? deepFreeze(checkedConfig) : config;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setLovelaceConfig(config: LovelaceConfig, mode: Lovelace["mode"]) {
|
private _setLovelaceConfig(
|
||||||
|
config: LovelaceConfig,
|
||||||
|
rawConfig: LovelaceConfig | undefined,
|
||||||
|
mode: Lovelace["mode"]
|
||||||
|
) {
|
||||||
config = this._checkLovelaceConfig(config);
|
config = this._checkLovelaceConfig(config);
|
||||||
const urlPath = this.urlPath;
|
const urlPath = this.urlPath;
|
||||||
this.lovelace = {
|
this.lovelace = {
|
||||||
config,
|
config,
|
||||||
|
rawConfig,
|
||||||
mode,
|
mode,
|
||||||
urlPath: this.urlPath,
|
urlPath: this.urlPath,
|
||||||
editMode: this.lovelace ? this.lovelace.editMode : false,
|
editMode: this.lovelace ? this.lovelace.editMode : false,
|
||||||
@ -294,22 +335,39 @@ class LovelacePanel extends LitElement {
|
|||||||
this._state = "yaml-editor";
|
this._state = "yaml-editor";
|
||||||
},
|
},
|
||||||
setEditMode: (editMode: boolean) => {
|
setEditMode: (editMode: boolean) => {
|
||||||
|
// If we use a strategy for dashboard, we cannot show the edit UI
|
||||||
|
// So go straight to the YAML editor
|
||||||
|
if (
|
||||||
|
this.lovelace!.rawConfig &&
|
||||||
|
this.lovelace!.rawConfig !== this.lovelace!.config
|
||||||
|
) {
|
||||||
|
this.lovelace!.enableFullEditMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!editMode || this.lovelace!.mode !== "generated") {
|
if (!editMode || this.lovelace!.mode !== "generated") {
|
||||||
this._updateLovelace({ editMode });
|
this._updateLovelace({ editMode });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showSaveDialog(this, {
|
showSaveDialog(this, {
|
||||||
lovelace: this.lovelace!,
|
lovelace: this.lovelace!,
|
||||||
mode: this.panel!.config.mode,
|
mode: this.panel!.config.mode,
|
||||||
|
narrow: this.narrow!,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
saveConfig: async (newConfig: LovelaceConfig): Promise<void> => {
|
saveConfig: async (newConfig: LovelaceConfig): Promise<void> => {
|
||||||
const { config: previousConfig, mode: previousMode } = this.lovelace!;
|
const {
|
||||||
|
config: previousConfig,
|
||||||
|
rawConfig: previousRawConfig,
|
||||||
|
mode: previousMode,
|
||||||
|
} = this.lovelace!;
|
||||||
newConfig = this._checkLovelaceConfig(newConfig);
|
newConfig = this._checkLovelaceConfig(newConfig);
|
||||||
try {
|
try {
|
||||||
// Optimistic update
|
// Optimistic update
|
||||||
this._updateLovelace({
|
this._updateLovelace({
|
||||||
config: newConfig,
|
config: newConfig,
|
||||||
|
rawConfig: undefined,
|
||||||
mode: "storage",
|
mode: "storage",
|
||||||
});
|
});
|
||||||
this._ignoreNextUpdateEvent = true;
|
this._ignoreNextUpdateEvent = true;
|
||||||
@ -320,18 +378,30 @@ class LovelacePanel extends LitElement {
|
|||||||
// Rollback the optimistic update
|
// Rollback the optimistic update
|
||||||
this._updateLovelace({
|
this._updateLovelace({
|
||||||
config: previousConfig,
|
config: previousConfig,
|
||||||
|
rawConfig: previousRawConfig,
|
||||||
mode: previousMode,
|
mode: previousMode,
|
||||||
});
|
});
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteConfig: async (): Promise<void> => {
|
deleteConfig: async (): Promise<void> => {
|
||||||
const { config: previousConfig, mode: previousMode } = this.lovelace!;
|
const {
|
||||||
|
config: previousConfig,
|
||||||
|
rawConfig: previousRawConfig,
|
||||||
|
mode: previousMode,
|
||||||
|
} = this.lovelace!;
|
||||||
try {
|
try {
|
||||||
// Optimistic update
|
// Optimistic update
|
||||||
const localize = await this.hass!.loadBackendTranslation("title");
|
const generatedConf = await generateLovelaceDashboardStrategy(
|
||||||
|
{
|
||||||
|
hass: this.hass!,
|
||||||
|
narrow: this.narrow,
|
||||||
|
},
|
||||||
|
DEFAULT_STRATEGY
|
||||||
|
);
|
||||||
this._updateLovelace({
|
this._updateLovelace({
|
||||||
config: await generateLovelaceConfigFromHass(this.hass!, localize),
|
config: generatedConf,
|
||||||
|
rawConfig: undefined,
|
||||||
mode: "generated",
|
mode: "generated",
|
||||||
editMode: false,
|
editMode: false,
|
||||||
});
|
});
|
||||||
@ -343,6 +413,7 @@ class LovelacePanel extends LitElement {
|
|||||||
// Rollback the optimistic update
|
// Rollback the optimistic update
|
||||||
this._updateLovelace({
|
this._updateLovelace({
|
||||||
config: previousConfig,
|
config: previousConfig,
|
||||||
|
rawConfig: previousRawConfig,
|
||||||
mode: previousMode,
|
mode: previousMode,
|
||||||
});
|
});
|
||||||
throw err;
|
throw err;
|
||||||
@ -356,6 +427,18 @@ class LovelacePanel extends LitElement {
|
|||||||
...this.lovelace!,
|
...this.lovelace!,
|
||||||
...props,
|
...props,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ("editMode" in props) {
|
||||||
|
window.history.replaceState(
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
constructUrlCurrentPath(
|
||||||
|
props.editMode
|
||||||
|
? addSearchParam({ edit: "1" })
|
||||||
|
: removeSearchParam("edit")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ class LovelaceFullConfigEditor extends LitElement {
|
|||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
this.yamlEditor.value = safeDump(this.lovelace!.config);
|
this.yamlEditor.value = safeDump(this.lovelace!.rawConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
@ -43,9 +43,7 @@ import { navigate } from "../../common/navigate";
|
|||||||
import {
|
import {
|
||||||
addSearchParam,
|
addSearchParam,
|
||||||
extractSearchParam,
|
extractSearchParam,
|
||||||
removeSearchParam,
|
|
||||||
} from "../../common/url/search-params";
|
} from "../../common/url/search-params";
|
||||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
|
||||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||||
import { debounce } from "../../common/util/debounce";
|
import { debounce } from "../../common/util/debounce";
|
||||||
import { afterNextRender } from "../../common/util/render-status";
|
import { afterNextRender } from "../../common/util/render-status";
|
||||||
@ -539,7 +537,7 @@ class HUIRoot extends LitElement {
|
|||||||
protected firstUpdated() {
|
protected firstUpdated() {
|
||||||
// Check for requested edit mode
|
// Check for requested edit mode
|
||||||
if (extractSearchParam("edit") === "1") {
|
if (extractSearchParam("edit") === "1") {
|
||||||
this._enableEditMode();
|
this.lovelace!.setEditMode(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -715,25 +713,11 @@ class HUIRoot extends LitElement {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._enableEditMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _enableEditMode(): void {
|
|
||||||
this.lovelace!.setEditMode(true);
|
this.lovelace!.setEditMode(true);
|
||||||
window.history.replaceState(
|
|
||||||
null,
|
|
||||||
"",
|
|
||||||
constructUrlCurrentPath(addSearchParam({ edit: "1" }))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _editModeDisable(): void {
|
private _editModeDisable(): void {
|
||||||
this.lovelace!.setEditMode(false);
|
this.lovelace!.setEditMode(false);
|
||||||
window.history.replaceState(
|
|
||||||
null,
|
|
||||||
"",
|
|
||||||
constructUrlCurrentPath(removeSearchParam("edit"))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _editLovelace() {
|
private _editLovelace() {
|
||||||
@ -837,7 +821,7 @@ class HUIRoot extends LitElement {
|
|||||||
const viewConfig = this.config.views[viewIndex];
|
const viewConfig = this.config.views[viewIndex];
|
||||||
|
|
||||||
if (!viewConfig) {
|
if (!viewConfig) {
|
||||||
this._enableEditMode();
|
this.lovelace!.setEditMode(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
158
src/panels/lovelace/strategies/get-strategy.ts
Normal file
158
src/panels/lovelace/strategies/get-strategy.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { LovelaceConfig, LovelaceViewConfig } from "../../../data/lovelace";
|
||||||
|
import { AsyncReturnType, HomeAssistant } from "../../../types";
|
||||||
|
import { OriginalStatesStrategy } from "./original-states-strategy";
|
||||||
|
|
||||||
|
const MAX_WAIT_STRATEGY_LOAD = 5000;
|
||||||
|
const CUSTOM_PREFIX = "custom:";
|
||||||
|
|
||||||
|
export interface LovelaceDashboardStrategy {
|
||||||
|
generateDashboard(info: {
|
||||||
|
config?: LovelaceConfig;
|
||||||
|
hass: HomeAssistant;
|
||||||
|
narrow: boolean | undefined;
|
||||||
|
}): Promise<LovelaceConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LovelaceViewStrategy {
|
||||||
|
generateView(info: {
|
||||||
|
view: LovelaceViewConfig;
|
||||||
|
config: LovelaceConfig;
|
||||||
|
hass: HomeAssistant;
|
||||||
|
narrow: boolean | undefined;
|
||||||
|
}): Promise<LovelaceViewConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strategies: Record<
|
||||||
|
string,
|
||||||
|
LovelaceDashboardStrategy & LovelaceViewStrategy
|
||||||
|
> = {
|
||||||
|
"original-states": OriginalStatesStrategy,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLovelaceStrategy = async <
|
||||||
|
T extends LovelaceDashboardStrategy | LovelaceViewStrategy
|
||||||
|
>(
|
||||||
|
name: string
|
||||||
|
): Promise<T> => {
|
||||||
|
if (name in strategies) {
|
||||||
|
return strategies[name] as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name.startsWith(CUSTOM_PREFIX)) {
|
||||||
|
throw new Error("Unknown strategy");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag = `ll-strategy-${name.substr(CUSTOM_PREFIX.length)}`;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(await Promise.race([
|
||||||
|
customElements.whenDefined(tag),
|
||||||
|
new Promise((resolve) =>
|
||||||
|
setTimeout(() => resolve(true), MAX_WAIT_STRATEGY_LOAD)
|
||||||
|
),
|
||||||
|
])) === true
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Timeout waiting for strategy element ${tag} to be registered`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return customElements.get(tag);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GenerateMethods {
|
||||||
|
generateDashboard: LovelaceDashboardStrategy["generateDashboard"];
|
||||||
|
generateView: LovelaceViewStrategy["generateView"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateStrategy = async <T extends keyof GenerateMethods>(
|
||||||
|
generateMethod: T,
|
||||||
|
renderError: (err: string | Error) => AsyncReturnType<GenerateMethods[T]>,
|
||||||
|
info: Parameters<GenerateMethods[T]>[0],
|
||||||
|
name: string | undefined
|
||||||
|
): Promise<ReturnType<GenerateMethods[T]>> => {
|
||||||
|
if (!name) {
|
||||||
|
return renderError("No strategy name found");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const strategy = (await getLovelaceStrategy(name)) as any;
|
||||||
|
return await strategy[generateMethod](info);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message !== "timeout") {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderError(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateLovelaceDashboardStrategy = async (
|
||||||
|
info: Parameters<LovelaceDashboardStrategy["generateDashboard"]>[0],
|
||||||
|
name?: string
|
||||||
|
): ReturnType<LovelaceDashboardStrategy["generateDashboard"]> =>
|
||||||
|
generateStrategy(
|
||||||
|
"generateDashboard",
|
||||||
|
(err) => ({
|
||||||
|
views: [
|
||||||
|
{
|
||||||
|
title: "Error",
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
type: "markdown",
|
||||||
|
content: `Error loading the dashboard strategy:\n> ${err}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
info,
|
||||||
|
name || info.config?.strategy?.name
|
||||||
|
);
|
||||||
|
|
||||||
|
export const generateLovelaceViewStrategy = async (
|
||||||
|
info: Parameters<LovelaceViewStrategy["generateView"]>[0],
|
||||||
|
name?: string
|
||||||
|
): ReturnType<LovelaceViewStrategy["generateView"]> =>
|
||||||
|
generateStrategy(
|
||||||
|
"generateView",
|
||||||
|
(err) => ({
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
type: "markdown",
|
||||||
|
content: `Error loading the view strategy:\n> ${err}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
info,
|
||||||
|
name || info.view?.strategy?.name
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all references to strategies and replaces them with the generated output
|
||||||
|
*/
|
||||||
|
export const expandLovelaceConfigStrategies = async (
|
||||||
|
info: Parameters<LovelaceDashboardStrategy["generateDashboard"]>[0] & {
|
||||||
|
config: LovelaceConfig;
|
||||||
|
}
|
||||||
|
): Promise<LovelaceConfig> => {
|
||||||
|
const config = info.config.strategy
|
||||||
|
? await generateLovelaceDashboardStrategy(info)
|
||||||
|
: { ...info.config };
|
||||||
|
|
||||||
|
config.views = await Promise.all(
|
||||||
|
config.views.map((view) =>
|
||||||
|
view.strategy
|
||||||
|
? generateLovelaceViewStrategy({
|
||||||
|
hass: info.hass,
|
||||||
|
narrow: info.narrow,
|
||||||
|
config,
|
||||||
|
view,
|
||||||
|
})
|
||||||
|
: view
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
94
src/panels/lovelace/strategies/original-states-strategy.ts
Normal file
94
src/panels/lovelace/strategies/original-states-strategy.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { STATE_NOT_RUNNING } from "home-assistant-js-websocket";
|
||||||
|
import { subscribeOne } from "../../../common/util/subscribe-one";
|
||||||
|
import { subscribeAreaRegistry } from "../../../data/area_registry";
|
||||||
|
import { subscribeDeviceRegistry } from "../../../data/device_registry";
|
||||||
|
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||||
|
import { generateDefaultViewConfig } from "../common/generate-lovelace-config";
|
||||||
|
import {
|
||||||
|
LovelaceViewStrategy,
|
||||||
|
LovelaceDashboardStrategy,
|
||||||
|
} from "./get-strategy";
|
||||||
|
|
||||||
|
let subscribedRegistries = false;
|
||||||
|
|
||||||
|
export class OriginalStatesStrategy {
|
||||||
|
static async generateView(
|
||||||
|
info: Parameters<LovelaceViewStrategy["generateView"]>[0]
|
||||||
|
): ReturnType<LovelaceViewStrategy["generateView"]> {
|
||||||
|
const hass = info.hass;
|
||||||
|
|
||||||
|
if (hass.config.state === STATE_NOT_RUNNING) {
|
||||||
|
return {
|
||||||
|
cards: [{ type: "starting" }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hass.config.safe_mode) {
|
||||||
|
return {
|
||||||
|
cards: [{ type: "safe-mode" }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// We leave this here so we always have the freshest data.
|
||||||
|
if (!subscribedRegistries) {
|
||||||
|
subscribedRegistries = true;
|
||||||
|
subscribeAreaRegistry(hass.connection, () => undefined);
|
||||||
|
subscribeDeviceRegistry(hass.connection, () => undefined);
|
||||||
|
subscribeEntityRegistry(hass.connection, () => undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [
|
||||||
|
areaEntries,
|
||||||
|
deviceEntries,
|
||||||
|
entityEntries,
|
||||||
|
localize,
|
||||||
|
] = await Promise.all([
|
||||||
|
subscribeOne(hass.connection, subscribeAreaRegistry),
|
||||||
|
subscribeOne(hass.connection, subscribeDeviceRegistry),
|
||||||
|
subscribeOne(hass.connection, subscribeEntityRegistry),
|
||||||
|
hass.loadBackendTranslation("title"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// User can override default view. If they didn't, we will add one
|
||||||
|
// that contains all entities.
|
||||||
|
const view = generateDefaultViewConfig(
|
||||||
|
areaEntries,
|
||||||
|
deviceEntries,
|
||||||
|
entityEntries,
|
||||||
|
hass.states,
|
||||||
|
localize
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add map of geo locations to default view if loaded
|
||||||
|
if (hass.config.components.includes("geo_location")) {
|
||||||
|
if (view && view.cards) {
|
||||||
|
view.cards.push({
|
||||||
|
type: "map",
|
||||||
|
geo_location_sources: ["all"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// User has no entities
|
||||||
|
if (view.cards!.length === 0) {
|
||||||
|
view.cards!.push({
|
||||||
|
type: "empty-state",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async generateDashboard(
|
||||||
|
info: Parameters<LovelaceDashboardStrategy["generateDashboard"]>[0]
|
||||||
|
): ReturnType<LovelaceDashboardStrategy["generateDashboard"]> {
|
||||||
|
return {
|
||||||
|
views: [
|
||||||
|
{
|
||||||
|
strategy: { name: "original-states" },
|
||||||
|
title: info.hass.config.location_name,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,8 @@ declare global {
|
|||||||
|
|
||||||
export interface Lovelace {
|
export interface Lovelace {
|
||||||
config: LovelaceConfig;
|
config: LovelaceConfig;
|
||||||
|
// If not set, a strategy was used to generate everything
|
||||||
|
rawConfig: LovelaceConfig | undefined;
|
||||||
editMode: boolean;
|
editMode: boolean;
|
||||||
urlPath: string | null;
|
urlPath: string | null;
|
||||||
mode: "generated" | "yaml" | "storage";
|
mode: "generated" | "yaml" | "storage";
|
||||||
|
@ -53,6 +53,8 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
|||||||
|
|
||||||
@property({ type: Number }) public index?: number;
|
@property({ type: Number }) public index?: number;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public isStrategy = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public cards: Array<
|
@property({ attribute: false }) public cards: Array<
|
||||||
LovelaceCard | HuiErrorCard
|
LovelaceCard | HuiErrorCard
|
||||||
> = [];
|
> = [];
|
||||||
@ -228,7 +230,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
|||||||
|
|
||||||
private _addCardToColumn(columnEl, index, editMode) {
|
private _addCardToColumn(columnEl, index, editMode) {
|
||||||
const card: LovelaceCard = this.cards[index];
|
const card: LovelaceCard = this.cards[index];
|
||||||
if (!editMode) {
|
if (!editMode || this.isStrategy) {
|
||||||
card.editMode = false;
|
card.editMode = false;
|
||||||
columnEl.appendChild(card);
|
columnEl.appendChild(card);
|
||||||
} else {
|
} else {
|
||||||
|
@ -31,6 +31,8 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
|||||||
|
|
||||||
@property({ type: Number }) public index?: number;
|
@property({ type: Number }) public index?: number;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public isStrategy = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public cards: Array<
|
@property({ attribute: false }) public cards: Array<
|
||||||
LovelaceCard | HuiErrorCard
|
LovelaceCard | HuiErrorCard
|
||||||
> = [];
|
> = [];
|
||||||
@ -109,7 +111,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
|||||||
const card: LovelaceCard = this.cards[0];
|
const card: LovelaceCard = this.cards[0];
|
||||||
card.isPanel = true;
|
card.isPanel = true;
|
||||||
|
|
||||||
if (!this.lovelace?.editMode) {
|
if (this.isStrategy || !this.lovelace?.editMode) {
|
||||||
card.editMode = false;
|
card.editMode = false;
|
||||||
this._card = card;
|
this._card = card;
|
||||||
return;
|
return;
|
||||||
|
@ -23,6 +23,7 @@ import { createViewElement } from "../create-element/create-view-element";
|
|||||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||||
import { confDeleteCard } from "../editor/delete-card";
|
import { confDeleteCard } from "../editor/delete-card";
|
||||||
|
import { generateLovelaceViewStrategy } from "../strategies/get-strategy";
|
||||||
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
|
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
|
||||||
|
|
||||||
const DEFAULT_VIEW_LAYOUT = "masonry";
|
const DEFAULT_VIEW_LAYOUT = "masonry";
|
||||||
@ -55,6 +56,8 @@ export class HUIView extends UpdatingElement {
|
|||||||
|
|
||||||
private _layoutElement?: LovelaceViewElement;
|
private _layoutElement?: LovelaceViewElement;
|
||||||
|
|
||||||
|
private _viewConfigTheme?: string;
|
||||||
|
|
||||||
// Public to make demo happy
|
// Public to make demo happy
|
||||||
public createCardElement(cardConfig: LovelaceCardConfig) {
|
public createCardElement(cardConfig: LovelaceCardConfig) {
|
||||||
const element = createCardElement(cardConfig) as LovelaceCard;
|
const element = createCardElement(cardConfig) as LovelaceCard;
|
||||||
@ -100,51 +103,21 @@ export class HUIView extends UpdatingElement {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const oldLovelace = changedProperties.get("lovelace") as this["lovelace"];
|
const oldLovelace = changedProperties.get("lovelace") as this["lovelace"];
|
||||||
const configChanged =
|
|
||||||
|
// If config has changed, create element if necessary and set all values.
|
||||||
|
if (
|
||||||
changedProperties.has("index") ||
|
changedProperties.has("index") ||
|
||||||
(changedProperties.has("lovelace") &&
|
(changedProperties.has("lovelace") &&
|
||||||
(!oldLovelace ||
|
(!oldLovelace ||
|
||||||
this.lovelace.config.views[this.index] !==
|
this.lovelace.config.views[this.index] !==
|
||||||
oldLovelace.config.views[this.index]));
|
oldLovelace.config.views[this.index]))
|
||||||
|
) {
|
||||||
|
this._initializeConfig();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If config has changed, create element if necessary and set all values.
|
// If no layout element, we're still creating one
|
||||||
if (configChanged) {
|
if (this._layoutElement) {
|
||||||
let viewConfig = this.lovelace.config.views[this.index];
|
|
||||||
viewConfig = {
|
|
||||||
...viewConfig,
|
|
||||||
type: viewConfig.panel
|
|
||||||
? PANEL_VIEW_LAYOUT
|
|
||||||
: viewConfig.type || DEFAULT_VIEW_LAYOUT,
|
|
||||||
};
|
|
||||||
|
|
||||||
this._createBadges(viewConfig!);
|
|
||||||
this._createCards(viewConfig!);
|
|
||||||
|
|
||||||
// Create a new layout element if necessary.
|
|
||||||
let addLayoutElement = false;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!this._layoutElement ||
|
|
||||||
this._layoutElementType !== viewConfig!.type
|
|
||||||
) {
|
|
||||||
addLayoutElement = true;
|
|
||||||
this._createLayoutElement(viewConfig!);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._layoutElement!.hass = this.hass;
|
|
||||||
this._layoutElement!.narrow = this.narrow;
|
|
||||||
this._layoutElement!.lovelace = this.lovelace;
|
|
||||||
this._layoutElement!.index = this.index;
|
|
||||||
this._layoutElement!.cards = this._cards;
|
|
||||||
this._layoutElement!.badges = this._badges;
|
|
||||||
|
|
||||||
if (addLayoutElement) {
|
|
||||||
while (this.lastChild) {
|
|
||||||
this.removeChild(this.lastChild);
|
|
||||||
}
|
|
||||||
this.appendChild(this._layoutElement!);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Config has not changed. Just props
|
// Config has not changed. Just props
|
||||||
if (changedProperties.has("hass")) {
|
if (changedProperties.has("hass")) {
|
||||||
this._badges.forEach((badge) => {
|
this._badges.forEach((badge) => {
|
||||||
@ -155,56 +128,98 @@ export class HUIView extends UpdatingElement {
|
|||||||
element.hass = this.hass;
|
element.hass = this.hass;
|
||||||
});
|
});
|
||||||
|
|
||||||
this._layoutElement!.hass = this.hass;
|
this._layoutElement.hass = this.hass;
|
||||||
}
|
}
|
||||||
if (changedProperties.has("narrow")) {
|
if (changedProperties.has("narrow")) {
|
||||||
this._layoutElement!.narrow = this.narrow;
|
this._layoutElement.narrow = this.narrow;
|
||||||
}
|
}
|
||||||
if (changedProperties.has("lovelace")) {
|
if (changedProperties.has("lovelace")) {
|
||||||
this._layoutElement!.lovelace = this.lovelace;
|
this._layoutElement.lovelace = this.lovelace;
|
||||||
}
|
}
|
||||||
if (changedProperties.has("_cards")) {
|
if (changedProperties.has("_cards")) {
|
||||||
this._layoutElement!.cards = this._cards;
|
this._layoutElement.cards = this._cards;
|
||||||
}
|
}
|
||||||
if (changedProperties.has("_badges")) {
|
if (changedProperties.has("_badges")) {
|
||||||
this._layoutElement!.badges = this._badges;
|
this._layoutElement.badges = this._badges;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldHass = changedProperties.get("hass") as this["hass"] | undefined;
|
const oldHass = changedProperties.get("hass") as this["hass"] | undefined;
|
||||||
|
|
||||||
// Update theme if necessary:
|
|
||||||
// - If config changed, the theme could have changed
|
|
||||||
// - if hass themes preferences have changed
|
|
||||||
if (
|
if (
|
||||||
configChanged ||
|
changedProperties.has("hass") &&
|
||||||
(changedProperties.has("hass") &&
|
(!oldHass ||
|
||||||
(!oldHass ||
|
this.hass.themes !== oldHass.themes ||
|
||||||
this.hass.themes !== oldHass.themes ||
|
this.hass.selectedTheme !== oldHass.selectedTheme)
|
||||||
this.hass.selectedTheme !== oldHass.selectedTheme))
|
|
||||||
) {
|
) {
|
||||||
applyThemesOnElement(
|
applyThemesOnElement(this, this.hass.themes, this._viewConfigTheme);
|
||||||
this,
|
}
|
||||||
this.hass.themes,
|
}
|
||||||
this.lovelace.config.views[this.index].theme
|
|
||||||
);
|
private async _initializeConfig() {
|
||||||
|
let viewConfig = this.lovelace.config.views[this.index];
|
||||||
|
let isStrategy = false;
|
||||||
|
|
||||||
|
if (viewConfig.strategy) {
|
||||||
|
isStrategy = true;
|
||||||
|
viewConfig = await generateLovelaceViewStrategy({
|
||||||
|
hass: this.hass,
|
||||||
|
config: this.lovelace.config,
|
||||||
|
narrow: this.narrow,
|
||||||
|
view: viewConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
viewConfig = {
|
||||||
|
...viewConfig,
|
||||||
|
type: viewConfig.panel
|
||||||
|
? PANEL_VIEW_LAYOUT
|
||||||
|
: viewConfig.type || DEFAULT_VIEW_LAYOUT,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a new layout element if necessary.
|
||||||
|
let addLayoutElement = false;
|
||||||
|
|
||||||
|
if (!this._layoutElement || this._layoutElementType !== viewConfig.type) {
|
||||||
|
addLayoutElement = true;
|
||||||
|
this._createLayoutElement(viewConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._createBadges(viewConfig);
|
||||||
|
this._createCards(viewConfig);
|
||||||
|
this._layoutElement!.isStrategy = isStrategy;
|
||||||
|
this._layoutElement!.hass = this.hass;
|
||||||
|
this._layoutElement!.narrow = this.narrow;
|
||||||
|
this._layoutElement!.lovelace = this.lovelace;
|
||||||
|
this._layoutElement!.index = this.index;
|
||||||
|
this._layoutElement!.cards = this._cards;
|
||||||
|
this._layoutElement!.badges = this._badges;
|
||||||
|
|
||||||
|
applyThemesOnElement(this, this.hass.themes, viewConfig.theme);
|
||||||
|
this._viewConfigTheme = viewConfig.theme;
|
||||||
|
|
||||||
|
if (addLayoutElement) {
|
||||||
|
while (this.lastChild) {
|
||||||
|
this.removeChild(this.lastChild);
|
||||||
|
}
|
||||||
|
this.appendChild(this._layoutElement!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createLayoutElement(config: LovelaceViewConfig): void {
|
private _createLayoutElement(config: LovelaceViewConfig): void {
|
||||||
this._layoutElement = createViewElement(config);
|
this._layoutElement = createViewElement(config) as LovelaceViewElement;
|
||||||
this._layoutElementType = config.type;
|
this._layoutElementType = config.type;
|
||||||
this._layoutElement.addEventListener("ll-create-card", () => {
|
this._layoutElement.addEventListener("ll-create-card", () => {
|
||||||
showCreateCardDialog(this, {
|
showCreateCardDialog(this, {
|
||||||
lovelaceConfig: this.lovelace!.config,
|
lovelaceConfig: this.lovelace.config,
|
||||||
saveConfig: this.lovelace!.saveConfig,
|
saveConfig: this.lovelace.saveConfig,
|
||||||
path: [this.index],
|
path: [this.index],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
|
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
|
||||||
showEditCardDialog(this, {
|
showEditCardDialog(this, {
|
||||||
lovelaceConfig: this.lovelace!.config,
|
lovelaceConfig: this.lovelace.config,
|
||||||
saveConfig: this.lovelace!.saveConfig,
|
saveConfig: this.lovelace.saveConfig,
|
||||||
path: ev.detail.path,
|
path: ev.detail.path,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -256,3 +256,12 @@ export interface LocalizeMixin {
|
|||||||
hass?: HomeAssistant;
|
hass?: HomeAssistant;
|
||||||
localize: LocalizeFunc;
|
localize: LocalizeFunc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://www.jpwilliams.dev/how-to-unpack-the-return-type-of-a-promise-in-typescript
|
||||||
|
export type AsyncReturnType<T extends (...args: any) => any> = T extends (
|
||||||
|
...args: any
|
||||||
|
) => Promise<infer U>
|
||||||
|
? U
|
||||||
|
: T extends (...args: any) => infer U
|
||||||
|
? U
|
||||||
|
: never;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user