Sketch out strategies (#8959)

Co-authored-by: Zack Arnett <arnett.zackary@gmail.com>
This commit is contained in:
Paulus Schoutsen 2021-04-23 09:36:45 -07:00 committed by GitHub
parent b599417a37
commit 63e10314bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 509 additions and 287 deletions

View File

@ -35,6 +35,7 @@ class HcLovelace extends LitElement {
}
const lovelace: Lovelace = {
config: this.lovelaceConfig,
rawConfig: this.lovelaceConfig,
editMode: false,
urlPath: this.urlPath!,
enableFullEditMode: () => undefined,

View File

@ -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"
)
);
}

View File

@ -19,6 +19,10 @@ export interface LovelacePanelConfig {
export interface LovelaceConfig {
title?: string;
strategy?: {
name: string;
options?: Record<string, unknown>;
};
views: LovelaceViewConfig[];
background?: string;
}
@ -77,6 +81,10 @@ export interface LovelaceViewConfig {
index?: number;
title?: string;
type?: string;
strategy?: {
name: string;
options?: Record<string, unknown>;
};
badges?: Array<string | LovelaceBadgeConfig>;
cards?: LovelaceCardConfig[];
path?: string;
@ -94,6 +102,7 @@ export interface LovelaceViewElement extends HTMLElement {
index?: number;
cards?: Array<LovelaceCard | HuiErrorCard>;
badges?: LovelaceBadge[];
isStrategy: boolean;
setConfig(config: LovelaceViewConfig): void;
}

View File

@ -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>` : ""}
`;
}

View File

@ -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,
@ -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;
@ -239,7 +212,7 @@ const computeDefaultViewStates = (
return states;
};
const generateViewConfig = (
export const generateViewConfig = (
localize: LocalizeFunc,
path: string,
title: string | undefined,
@ -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
);
};

View File

@ -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";
@ -13,7 +14,7 @@ const LAZY_LOAD_LAYOUTS = {
export const createViewElement = (
config: LovelaceViewConfig
): LovelaceViewElement => {
): LovelaceViewElement | HuiErrorCard => {
return createLovelaceElement(
"view",
config,

View File

@ -19,13 +19,15 @@ import "../../../components/ha-formfield";
import "../../../components/ha-svg-icon";
import "../../../components/ha-switch";
import "../../../components/ha-yaml-editor";
import type { LovelaceConfig } from "../../../data/lovelace";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { expandLovelaceConfigStrategies } from "../strategies/get-strategy";
import type { SaveDialogParams } from "./show-save-config-dialog";
const EMPTY_CONFIG = { views: [] };
const EMPTY_CONFIG: LovelaceConfig = { views: [{ title: "Home" }] };
@customElement("hui-dialog-save-config")
export class HuiSaveConfig extends LitElement implements HassDialog {
@ -125,14 +127,17 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
</div>
${this._params.mode === "storage"
? html`
<mwc-button slot="primaryAction" @click=${this.closeDialog}
>${this.hass!.localize(
"ui.common.cancel"
)}
</mwc-button>
<mwc-button
slot="primaryAction"
.label=${this.hass!.localize("ui.common.cancel")}
@click=${this.closeDialog}
></mwc-button>
<mwc-button
slot="primaryAction"
?disabled=${this._saving}
aria-label=${this.hass!.localize(
"ui.panel.lovelace.editor.save_config.save"
)}
@click=${this._saveConfig}
>
${this._saving
@ -148,11 +153,13 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
</mwc-button>
`
: html`
<mwc-button slot="primaryAction" @click=${this.closeDialog}
>${this.hass!.localize(
<mwc-button
slot="primaryAction"
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.save_config.close"
)}
</mwc-button>
@click=${this.closeDialog}
></mwc-button>
`}
</ha-dialog>
`;
@ -177,7 +184,13 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
try {
const lovelace = this._params!.lovelace;
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);
this._saving = false;

View File

@ -14,6 +14,7 @@ const dialogTag = "hui-dialog-save-config";
export interface SaveDialogParams {
lovelace: Lovelace;
mode: "yaml" | "storage";
narrow: boolean;
}
let registeredDialog = false;

View File

@ -7,6 +7,11 @@ import {
property,
TemplateResult,
} from "lit-element";
import { constructUrlCurrentPath } from "../../common/url/construct-url";
import {
addSearchParam,
removeSearchParam,
} from "../../common/url/search-params";
import { domainToName } from "../../data/integration";
import {
deleteConfig,
@ -21,14 +26,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";
}
@ -71,7 +78,11 @@ class LovelacePanel extends LitElement {
this.lovelace.locale !== this.hass.locale
) {
// 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") {
// When lovelace is generated, we re-generate each time a user goes
// 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);
if (!this._unsubUpdates) {
this._subscribeUpdates();
@ -153,8 +166,14 @@ class LovelacePanel extends LitElement {
}
private async _regenerateConfig() {
const conf = await generateLovelaceConfigFromHass(this.hass!);
this._setLovelaceConfig(conf, "generated");
const conf = await generateLovelaceDashboardStrategy(
{
hass: this.hass!,
narrow: this.narrow,
},
DEFAULT_STRATEGY
);
this._setLovelaceConfig(conf, undefined, "generated");
this._state = "loaded";
}
@ -202,6 +221,7 @@ class LovelacePanel extends LitElement {
private async _fetchConfig(forceDiskRefresh: boolean) {
let conf: LovelaceConfig;
let rawConf: LovelaceConfig | undefined;
let confMode: Lovelace["mode"] = this.panel!.config.mode;
let confProm: Promise<LovelaceConfig> | undefined;
const llWindow = window as WindowWithLovelaceProm;
@ -236,7 +256,18 @@ class LovelacePanel extends LitElement {
}
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) {
if (err.code !== "config_not_found") {
// eslint-disable-next-line
@ -245,8 +276,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.
@ -258,7 +294,7 @@ class LovelacePanel extends LitElement {
}
this._state = this._state === "yaml-editor" ? this._state : "loaded";
this._setLovelaceConfig(conf, confMode);
this._setLovelaceConfig(conf, rawConf, confMode);
}
private _checkLovelaceConfig(config: LovelaceConfig) {
@ -277,11 +313,16 @@ class LovelacePanel extends LitElement {
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);
const urlPath = this.urlPath;
this.lovelace = {
config,
rawConfig,
mode,
urlPath: this.urlPath,
editMode: this.lovelace ? this.lovelace.editMode : false,
@ -294,22 +335,39 @@ class LovelacePanel extends LitElement {
this._state = "yaml-editor";
},
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") {
this._updateLovelace({ editMode });
return;
}
showSaveDialog(this, {
lovelace: this.lovelace!,
mode: this.panel!.config.mode,
narrow: this.narrow!,
});
},
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);
try {
// Optimistic update
this._updateLovelace({
config: newConfig,
rawConfig: undefined,
mode: "storage",
});
this._ignoreNextUpdateEvent = true;
@ -320,18 +378,30 @@ class LovelacePanel extends LitElement {
// Rollback the optimistic update
this._updateLovelace({
config: previousConfig,
rawConfig: previousRawConfig,
mode: previousMode,
});
throw err;
}
},
deleteConfig: async (): Promise<void> => {
const { config: previousConfig, mode: previousMode } = this.lovelace!;
const {
config: previousConfig,
rawConfig: previousRawConfig,
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,
rawConfig: undefined,
mode: "generated",
editMode: false,
});
@ -343,6 +413,7 @@ class LovelacePanel extends LitElement {
// Rollback the optimistic update
this._updateLovelace({
config: previousConfig,
rawConfig: previousRawConfig,
mode: previousMode,
});
throw err;
@ -356,6 +427,18 @@ class LovelacePanel extends LitElement {
...this.lovelace!,
...props,
};
if ("editMode" in props) {
window.history.replaceState(
null,
"",
constructUrlCurrentPath(
props.editMode
? addSearchParam({ edit: "1" })
: removeSearchParam("edit")
)
);
}
}
}

View File

@ -106,7 +106,7 @@ class LovelaceFullConfigEditor extends LitElement {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.yamlEditor.value = safeDump(this.lovelace!.config);
this.yamlEditor.value = safeDump(this.lovelace!.rawConfig);
}
protected updated(changedProps: PropertyValues) {

View File

@ -43,9 +43,7 @@ import { navigate } from "../../common/navigate";
import {
addSearchParam,
extractSearchParam,
removeSearchParam,
} from "../../common/url/search-params";
import { constructUrlCurrentPath } from "../../common/url/construct-url";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce";
import { afterNextRender } from "../../common/util/render-status";
@ -539,7 +537,7 @@ class HUIRoot extends LitElement {
protected firstUpdated() {
// Check for requested edit mode
if (extractSearchParam("edit") === "1") {
this._enableEditMode();
this.lovelace!.setEditMode(true);
}
}
@ -715,25 +713,11 @@ class HUIRoot extends LitElement {
});
return;
}
this._enableEditMode();
}
private _enableEditMode(): void {
this.lovelace!.setEditMode(true);
window.history.replaceState(
null,
"",
constructUrlCurrentPath(addSearchParam({ edit: "1" }))
);
}
private _editModeDisable(): void {
this.lovelace!.setEditMode(false);
window.history.replaceState(
null,
"",
constructUrlCurrentPath(removeSearchParam("edit"))
);
}
private _editLovelace() {
@ -837,7 +821,7 @@ class HUIRoot extends LitElement {
const viewConfig = this.config.views[viewIndex];
if (!viewConfig) {
this._enableEditMode();
this.lovelace!.setEditMode(true);
return;
}

View 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;
};

View 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,
},
],
};
}
}

View File

@ -18,6 +18,8 @@ declare global {
export interface Lovelace {
config: LovelaceConfig;
// If not set, a strategy was used to generate everything
rawConfig: LovelaceConfig | undefined;
editMode: boolean;
urlPath: string | null;
mode: "generated" | "yaml" | "storage";

View File

@ -53,6 +53,8 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
@property({ type: Number }) public index?: number;
@property({ type: Boolean }) public isStrategy = false;
@property({ attribute: false }) public cards: Array<
LovelaceCard | HuiErrorCard
> = [];
@ -228,7 +230,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
private _addCardToColumn(columnEl, index, editMode) {
const card: LovelaceCard = this.cards[index];
if (!editMode) {
if (!editMode || this.isStrategy) {
card.editMode = false;
columnEl.appendChild(card);
} else {

View File

@ -31,6 +31,8 @@ export class PanelView extends LitElement implements LovelaceViewElement {
@property({ type: Number }) public index?: number;
@property({ type: Boolean }) public isStrategy = false;
@property({ attribute: false }) public cards: Array<
LovelaceCard | HuiErrorCard
> = [];
@ -109,7 +111,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
const card: LovelaceCard = this.cards[0];
card.isPanel = true;
if (!this.lovelace?.editMode) {
if (this.isStrategy || !this.lovelace?.editMode) {
card.editMode = false;
this._card = card;
return;

View File

@ -23,6 +23,7 @@ import { createViewElement } from "../create-element/create-view-element";
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import { confDeleteCard } from "../editor/delete-card";
import { generateLovelaceViewStrategy } from "../strategies/get-strategy";
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
const DEFAULT_VIEW_LAYOUT = "masonry";
@ -55,6 +56,8 @@ export class HUIView extends UpdatingElement {
private _layoutElement?: LovelaceViewElement;
private _viewConfigTheme?: string;
// Public to make demo happy
public createCardElement(cardConfig: LovelaceCardConfig) {
const element = createCardElement(cardConfig) as LovelaceCard;
@ -100,51 +103,21 @@ export class HUIView extends UpdatingElement {
*/
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("lovelace") &&
(!oldLovelace ||
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 (configChanged) {
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 {
// If no layout element, we're still creating one
if (this._layoutElement) {
// Config has not changed. Just props
if (changedProperties.has("hass")) {
this._badges.forEach((badge) => {
@ -155,56 +128,98 @@ export class HUIView extends UpdatingElement {
element.hass = this.hass;
});
this._layoutElement!.hass = this.hass;
this._layoutElement.hass = this.hass;
}
if (changedProperties.has("narrow")) {
this._layoutElement!.narrow = this.narrow;
this._layoutElement.narrow = this.narrow;
}
if (changedProperties.has("lovelace")) {
this._layoutElement!.lovelace = this.lovelace;
this._layoutElement.lovelace = this.lovelace;
}
if (changedProperties.has("_cards")) {
this._layoutElement!.cards = this._cards;
this._layoutElement.cards = this._cards;
}
if (changedProperties.has("_badges")) {
this._layoutElement!.badges = this._badges;
this._layoutElement.badges = this._badges;
}
}
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 (
configChanged ||
(changedProperties.has("hass") &&
(!oldHass ||
this.hass.themes !== oldHass.themes ||
this.hass.selectedTheme !== oldHass.selectedTheme))
changedProperties.has("hass") &&
(!oldHass ||
this.hass.themes !== oldHass.themes ||
this.hass.selectedTheme !== oldHass.selectedTheme)
) {
applyThemesOnElement(
this,
this.hass.themes,
this.lovelace.config.views[this.index].theme
);
applyThemesOnElement(this, this.hass.themes, this._viewConfigTheme);
}
}
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 {
this._layoutElement = createViewElement(config);
this._layoutElement = createViewElement(config) as LovelaceViewElement;
this._layoutElementType = config.type;
this._layoutElement.addEventListener("ll-create-card", () => {
showCreateCardDialog(this, {
lovelaceConfig: this.lovelace!.config,
saveConfig: this.lovelace!.saveConfig,
lovelaceConfig: this.lovelace.config,
saveConfig: this.lovelace.saveConfig,
path: [this.index],
});
});
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
showEditCardDialog(this, {
lovelaceConfig: this.lovelace!.config,
saveConfig: this.lovelace!.saveConfig,
lovelaceConfig: this.lovelace.config,
saveConfig: this.lovelace.saveConfig,
path: ev.detail.path,
});
});

View File

@ -256,3 +256,12 @@ export interface LocalizeMixin {
hass?: HomeAssistant;
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;