Refactor strategy foundation (#17921)

pull/17982/head
Paul Bottein 2023-09-21 20:22:52 +02:00 committed by GitHub
parent 90d01e4b63
commit 9217d5bf40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 252 additions and 147 deletions

View File

@ -32,6 +32,8 @@ import { HassElement } from "../../../../src/state/hass-element";
import { castContext } from "../cast_context";
import "./hc-launch-screen";
const DEFAULT_STRATEGY = "original-states";
let resourcesLoaded = false;
@customElement("hc-main")
export class HcMain extends HassElement {
@ -258,7 +260,7 @@ export class HcMain extends HassElement {
{
strategy: {
type: "energy",
options: { show_date_selection: true },
show_date_selection: true,
},
},
],
@ -320,10 +322,10 @@ export class HcMain extends HassElement {
this._handleNewLovelaceConfig(
await generateLovelaceDashboardStrategy(
{
hass: this.hass!,
narrow: false,
type: DEFAULT_STRATEGY,
},
"original-states"
this.hass!,
{ narrow: false }
)
);
}

View File

@ -17,12 +17,14 @@ export interface LovelacePanelConfig {
mode: "yaml" | "storage";
}
export type LovelaceStrategyConfig = {
type: string;
[key: string]: any;
};
export interface LovelaceConfig {
title?: string;
strategy?: {
type: string;
options?: Record<string, unknown>;
};
strategy?: LovelaceStrategyConfig;
views: LovelaceViewConfig[];
background?: string;
}
@ -81,10 +83,7 @@ export interface LovelaceViewConfig {
index?: number;
title?: string;
type?: string;
strategy?: {
type: string;
options?: Record<string, unknown>;
};
strategy?: LovelaceStrategyConfig;
badges?: Array<string | LovelaceBadgeConfig>;
cards?: LovelaceCardConfig[];
path?: string;

View File

@ -1,10 +1,16 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
EnergyPreferences,
getEnergyPreferences,
GridSourceTypeEnergyPreference,
} from "../../../data/energy";
import { LovelaceViewConfig } from "../../../data/lovelace";
import { LovelaceViewStrategy } from "../../lovelace/strategies/get-strategy";
import {
LovelaceStrategyConfig,
LovelaceViewConfig,
} from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { LovelaceStrategyParams } from "../../lovelace/strategies/types";
const setupWizard = async (): Promise<LovelaceViewConfig> => {
await import("../cards/energy-setup-wizard-card");
@ -18,12 +24,17 @@ const setupWizard = async (): Promise<LovelaceViewConfig> => {
};
};
export class EnergyStrategy {
static async generateView(
info: Parameters<LovelaceViewStrategy["generateView"]>[0]
): ReturnType<LovelaceViewStrategy["generateView"]> {
const hass = info.hass;
export interface EnergeryViewStrategyConfig extends LovelaceStrategyConfig {
show_date_selection?: boolean;
}
@customElement("energy-view-strategy")
export class EnergyViewStrategy extends ReactiveElement {
static async generate(
config: EnergeryViewStrategyConfig,
hass: HomeAssistant,
params: LovelaceStrategyParams
): Promise<LovelaceViewConfig> {
const view: LovelaceViewConfig = { cards: [] };
let prefs: EnergyPreferences;
@ -56,7 +67,7 @@ export class EnergyStrategy {
(source) => source.type === "water"
);
if (info.narrow || info.view.strategy?.options?.show_date_selection) {
if (params.narrow || config.show_date_selection) {
view.cards!.push({
type: "energy-date-selection",
collection_key: "energy_dashboard",

View File

@ -174,10 +174,8 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
await lovelace.saveConfig(
this._emptyConfig
? EMPTY_CONFIG
: await expandLovelaceConfigStrategies({
config: lovelace.config,
hass: this.hass!,
narrow: this._params!.narrow,
: await expandLovelaceConfigStrategies(lovelace.config, this.hass, {
narrow: this._params.narrow,
})
);
lovelace.setEditMode(true);

View File

@ -165,10 +165,10 @@ export class LovelacePanel extends LitElement {
private async _regenerateConfig() {
const conf = await generateLovelaceDashboardStrategy(
{
hass: this.hass!,
narrow: this.narrow,
type: DEFAULT_STRATEGY,
},
DEFAULT_STRATEGY
this.hass!,
{ narrow: this.narrow }
);
this._setLovelaceConfig(conf, undefined, "generated");
this._panelState = "loaded";
@ -256,11 +256,11 @@ export class LovelacePanel extends LitElement {
// If strategy defined, apply it here.
if (rawConf.strategy) {
conf = await generateLovelaceDashboardStrategy({
config: rawConf,
hass: this.hass!,
narrow: this.narrow,
});
conf = await generateLovelaceDashboardStrategy(
rawConf.strategy,
this.hass!,
{ narrow: this.narrow }
);
} else {
conf = rawConf;
}
@ -274,10 +274,10 @@ export class LovelacePanel extends LitElement {
}
conf = await generateLovelaceDashboardStrategy(
{
hass: this.hass!,
narrow: this.narrow,
type: DEFAULT_STRATEGY,
},
DEFAULT_STRATEGY
this.hass!,
{ narrow: this.narrow }
);
confMode = "generated";
} finally {
@ -363,11 +363,11 @@ export class LovelacePanel extends LitElement {
let conf: LovelaceConfig;
// If strategy defined, apply it here.
if (newConfig.strategy) {
conf = await generateLovelaceDashboardStrategy({
config: newConfig,
hass: this.hass!,
narrow: this.narrow,
});
conf = await generateLovelaceDashboardStrategy(
newConfig.strategy,
this.hass!,
{ narrow: this.narrow }
);
} else {
conf = newConfig;
}
@ -402,10 +402,10 @@ export class LovelacePanel extends LitElement {
// Optimistic update
const generatedConf = await generateLovelaceDashboardStrategy(
{
hass: this.hass!,
narrow: this.narrow,
type: DEFAULT_STRATEGY,
},
DEFAULT_STRATEGY
this.hass!,
{ narrow: this.narrow }
);
this._updateLovelace({
config: generatedConf,

View File

@ -1,53 +1,63 @@
import { LovelaceConfig, LovelaceViewConfig } from "../../../data/lovelace";
import {
LovelaceConfig,
LovelaceStrategyConfig,
LovelaceViewConfig,
} from "../../../data/lovelace";
import { AsyncReturnType, HomeAssistant } from "../../../types";
import { isLegacyStrategy } from "./legacy-strategy";
import {
LovelaceDashboardStrategy,
LovelaceStrategy,
LovelaceStrategyParams,
LovelaceViewStrategy,
} from "./types";
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,
() => Promise<LovelaceDashboardStrategy | LovelaceViewStrategy>
> = {
"original-states": async () =>
(await import("./original-states-strategy")).OriginalStatesStrategy,
energy: async () =>
(await import("../../energy/strategies/energy-strategy")).EnergyStrategy,
const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
dashboard: {
"original-states": () => import("./original-states-dashboard-strategy"),
},
view: {
"original-states": () => import("./original-states-view-strategy"),
energy: () => import("../../energy/strategies/energy-view-strategy"),
},
};
const getLovelaceStrategy = async <
T extends LovelaceDashboardStrategy | LovelaceViewStrategy,
>(
export type LovelaceStrategyConfigType = "dashboard" | "view";
type Strategies = {
dashboard: LovelaceDashboardStrategy;
view: LovelaceViewStrategy;
};
type StrategyConfig<T extends LovelaceStrategyConfigType> = AsyncReturnType<
Strategies[T]["generate"]
>;
const getLovelaceStrategy = async <T extends LovelaceStrategyConfigType>(
configType: T,
strategyType: string
): Promise<T> => {
if (strategyType in strategies) {
return (await strategies[strategyType]()) as T;
): Promise<LovelaceStrategy> => {
if (strategyType in STRATEGIES[configType]) {
await STRATEGIES[configType][strategyType]();
const tag = `${strategyType}-${configType}-strategy`;
return customElements.get(tag) as unknown as Strategies[T];
}
if (!strategyType.startsWith(CUSTOM_PREFIX)) {
throw new Error("Unknown strategy");
}
const tag = `ll-strategy-${strategyType.substr(CUSTOM_PREFIX.length)}`;
const legacyTag = `ll-strategy-${strategyType.slice(CUSTOM_PREFIX.length)}`;
const tag = `ll-strategy-${configType}-${strategyType.slice(
CUSTOM_PREFIX.length
)}`;
if (
(await Promise.race([
customElements.whenDefined(legacyTag),
customElements.whenDefined(tag),
new Promise((resolve) => {
setTimeout(() => resolve(true), MAX_WAIT_STRATEGY_LOAD);
@ -59,29 +69,53 @@ const getLovelaceStrategy = async <
);
}
return customElements.get(tag) as unknown as T;
return (customElements.get(tag) ??
customElements.get(legacyTag)) as unknown as Strategies[T];
};
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],
strategyType: string | undefined
): Promise<ReturnType<GenerateMethods[T]>> => {
const generateStrategy = async <T extends LovelaceStrategyConfigType>(
configType: T,
renderError: (err: string | Error) => StrategyConfig<T>,
strategyConfig: LovelaceStrategyConfig,
hass: HomeAssistant,
params: LovelaceStrategyParams
): Promise<StrategyConfig<T>> => {
const strategyType = strategyConfig.type;
if (!strategyType) {
// @ts-ignore
return renderError("No strategy type found");
}
try {
const strategy = (await getLovelaceStrategy(strategyType)) as any;
// eslint-disable-next-line @typescript-eslint/return-await
return await strategy[generateMethod](info);
const strategy = await getLovelaceStrategy<T>(configType, strategyType);
// Backward compatibility for custom strategies for loading old strategies format
if (isLegacyStrategy(strategy)) {
if (configType === "dashboard" && "generateDashboard" in strategy) {
return (await strategy.generateDashboard({
config: { strategy: strategyConfig, views: [] },
hass,
narrow: params.narrow,
})) as StrategyConfig<T>;
}
if (configType === "view" && "generateView" in strategy) {
return (await strategy.generateView({
config: { views: [] },
view: { strategy: strategyConfig },
hass,
narrow: params.narrow,
})) as StrategyConfig<T>;
}
}
const config = {
...strategyConfig,
...strategyConfig.options,
};
delete config.options;
return await strategy.generate(config, hass, params);
} catch (err: any) {
if (err.message !== "timeout") {
// eslint-disable-next-line
@ -93,11 +127,12 @@ const generateStrategy = async <T extends keyof GenerateMethods>(
};
export const generateLovelaceDashboardStrategy = async (
info: Parameters<LovelaceDashboardStrategy["generateDashboard"]>[0],
strategyType?: string
): ReturnType<LovelaceDashboardStrategy["generateDashboard"]> =>
strategyConfig: LovelaceStrategyConfig,
hass: HomeAssistant,
params: LovelaceStrategyParams
): Promise<LovelaceConfig> =>
generateStrategy(
"generateDashboard",
"dashboard",
(err) => ({
views: [
{
@ -111,16 +146,18 @@ export const generateLovelaceDashboardStrategy = async (
},
],
}),
info,
strategyType || info.config?.strategy?.type
strategyConfig,
hass,
params
);
export const generateLovelaceViewStrategy = async (
info: Parameters<LovelaceViewStrategy["generateView"]>[0],
strategyType?: string
): ReturnType<LovelaceViewStrategy["generateView"]> =>
strategyConfig: LovelaceStrategyConfig,
hass: HomeAssistant,
params: LovelaceStrategyParams
): Promise<LovelaceViewConfig> =>
generateStrategy(
"generateView",
"view",
(err) => ({
cards: [
{
@ -129,34 +166,30 @@ export const generateLovelaceViewStrategy = async (
},
],
}),
info,
strategyType || info.view?.strategy?.type
strategyConfig,
hass,
params
);
/**
* Find all references to strategies and replaces them with the generated output
*/
export const expandLovelaceConfigStrategies = async (
info: Parameters<LovelaceDashboardStrategy["generateDashboard"]>[0] & {
config: LovelaceConfig;
}
config: LovelaceConfig,
hass: HomeAssistant,
params: LovelaceStrategyParams
): Promise<LovelaceConfig> => {
const config = info.config.strategy
? await generateLovelaceDashboardStrategy(info)
: { ...info.config };
const newConfig = config.strategy
? await generateLovelaceDashboardStrategy(config.strategy, hass, params)
: { ...config };
config.views = await Promise.all(
config.views.map((view) =>
newConfig.views = await Promise.all(
newConfig.views.map((view) =>
view.strategy
? generateLovelaceViewStrategy({
hass: info.hass,
narrow: info.narrow,
config,
view,
})
? generateLovelaceViewStrategy(view.strategy, hass, params)
: view
)
);
return config;
return newConfig;
};

View File

@ -0,0 +1,24 @@
import { LovelaceConfig, LovelaceViewConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
export const isLegacyStrategy = (
strategy: any
): strategy is LovelaceDashboardStrategy | LovelaceViewStrategy =>
!("generate" in strategy);
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>;
}

View File

@ -0,0 +1,23 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { LovelaceConfig, LovelaceStrategyConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { LovelaceStrategyParams } from "./types";
@customElement("original-states-dashboard-strategy")
export class OriginalStatesDashboardStrategy extends ReactiveElement {
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant,
_params?: LovelaceStrategyParams
): Promise<LovelaceConfig> {
return {
title: hass.config.location_name,
views: [
{
strategy: { type: "original-states" },
},
],
};
}
}

View File

@ -1,18 +1,23 @@
import { STATE_NOT_RUNNING } from "home-assistant-js-websocket";
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { getEnergyPreferences } from "../../../data/energy";
import { generateDefaultViewConfig } from "../common/generate-lovelace-config";
import {
LovelaceDashboardStrategy,
LovelaceViewStrategy,
} from "./get-strategy";
export class OriginalStatesStrategy {
static async generateView(
info: Parameters<LovelaceViewStrategy["generateView"]>[0]
): ReturnType<LovelaceViewStrategy["generateView"]> {
const hass = info.hass;
LovelaceStrategyConfig,
LovelaceViewConfig,
} from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { generateDefaultViewConfig } from "../common/generate-lovelace-config";
import { LovelaceStrategyParams } from "./types";
@customElement("original-states-view-strategy")
export class OriginalStatesViewStrategy extends ReactiveElement {
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant,
_params?: LovelaceStrategyParams
): Promise<LovelaceViewConfig> {
if (hass.config.state === STATE_NOT_RUNNING) {
return {
cards: [{ type: "starting" }],
@ -63,17 +68,4 @@ export class OriginalStatesStrategy {
return view;
}
static async generateDashboard(
info: Parameters<LovelaceDashboardStrategy["generateDashboard"]>[0]
): ReturnType<LovelaceDashboardStrategy["generateDashboard"]> {
return {
title: info.hass.config.location_name,
views: [
{
strategy: { type: "original-states" },
},
],
};
}
}

View File

@ -0,0 +1,24 @@
import {
LovelaceConfig,
LovelaceStrategyConfig,
LovelaceViewConfig,
} from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
export type LovelaceStrategyParams = {
narrow?: boolean;
};
export type LovelaceStrategy<T = any> = {
generate(
config: LovelaceStrategyConfig,
hass: HomeAssistant,
params?: LovelaceStrategyParams
): Promise<T>;
};
export interface LovelaceDashboardStrategy
extends LovelaceStrategy<LovelaceConfig> {}
export interface LovelaceViewStrategy
extends LovelaceStrategy<LovelaceViewConfig> {}

View File

@ -190,12 +190,11 @@ export class HUIView extends ReactiveElement {
if (viewConfig.strategy) {
isStrategy = true;
viewConfig = await generateLovelaceViewStrategy({
hass: this.hass,
config: this.lovelace.config,
narrow: this.narrow,
view: viewConfig,
});
viewConfig = await generateLovelaceViewStrategy(
viewConfig.strategy,
this.hass!,
{ narrow: this.narrow }
);
}
viewConfig = {