Refactor strategy foundation (#17921)

This commit is contained in:
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 { castContext } from "../cast_context";
import "./hc-launch-screen"; import "./hc-launch-screen";
const DEFAULT_STRATEGY = "original-states";
let resourcesLoaded = false; let resourcesLoaded = false;
@customElement("hc-main") @customElement("hc-main")
export class HcMain extends HassElement { export class HcMain extends HassElement {
@ -258,7 +260,7 @@ export class HcMain extends HassElement {
{ {
strategy: { strategy: {
type: "energy", type: "energy",
options: { show_date_selection: true }, show_date_selection: true,
}, },
}, },
], ],
@ -320,10 +322,10 @@ export class HcMain extends HassElement {
this._handleNewLovelaceConfig( this._handleNewLovelaceConfig(
await generateLovelaceDashboardStrategy( await generateLovelaceDashboardStrategy(
{ {
hass: this.hass!, type: DEFAULT_STRATEGY,
narrow: false,
}, },
"original-states" this.hass!,
{ narrow: false }
) )
); );
} }

View File

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

View File

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

View File

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

View File

@ -165,10 +165,10 @@ export class LovelacePanel extends LitElement {
private async _regenerateConfig() { private async _regenerateConfig() {
const conf = await generateLovelaceDashboardStrategy( const conf = await generateLovelaceDashboardStrategy(
{ {
hass: this.hass!, type: DEFAULT_STRATEGY,
narrow: this.narrow,
}, },
DEFAULT_STRATEGY this.hass!,
{ narrow: this.narrow }
); );
this._setLovelaceConfig(conf, undefined, "generated"); this._setLovelaceConfig(conf, undefined, "generated");
this._panelState = "loaded"; this._panelState = "loaded";
@ -256,11 +256,11 @@ export class LovelacePanel extends LitElement {
// If strategy defined, apply it here. // If strategy defined, apply it here.
if (rawConf.strategy) { if (rawConf.strategy) {
conf = await generateLovelaceDashboardStrategy({ conf = await generateLovelaceDashboardStrategy(
config: rawConf, rawConf.strategy,
hass: this.hass!, this.hass!,
narrow: this.narrow, { narrow: this.narrow }
}); );
} else { } else {
conf = rawConf; conf = rawConf;
} }
@ -274,10 +274,10 @@ export class LovelacePanel extends LitElement {
} }
conf = await generateLovelaceDashboardStrategy( conf = await generateLovelaceDashboardStrategy(
{ {
hass: this.hass!, type: DEFAULT_STRATEGY,
narrow: this.narrow,
}, },
DEFAULT_STRATEGY this.hass!,
{ narrow: this.narrow }
); );
confMode = "generated"; confMode = "generated";
} finally { } finally {
@ -363,11 +363,11 @@ export class LovelacePanel extends LitElement {
let conf: LovelaceConfig; let conf: LovelaceConfig;
// If strategy defined, apply it here. // If strategy defined, apply it here.
if (newConfig.strategy) { if (newConfig.strategy) {
conf = await generateLovelaceDashboardStrategy({ conf = await generateLovelaceDashboardStrategy(
config: newConfig, newConfig.strategy,
hass: this.hass!, this.hass!,
narrow: this.narrow, { narrow: this.narrow }
}); );
} else { } else {
conf = newConfig; conf = newConfig;
} }
@ -402,10 +402,10 @@ export class LovelacePanel extends LitElement {
// Optimistic update // Optimistic update
const generatedConf = await generateLovelaceDashboardStrategy( const generatedConf = await generateLovelaceDashboardStrategy(
{ {
hass: this.hass!, type: DEFAULT_STRATEGY,
narrow: this.narrow,
}, },
DEFAULT_STRATEGY this.hass!,
{ narrow: this.narrow }
); );
this._updateLovelace({ this._updateLovelace({
config: generatedConf, 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 { AsyncReturnType, HomeAssistant } from "../../../types";
import { isLegacyStrategy } from "./legacy-strategy";
import {
LovelaceDashboardStrategy,
LovelaceStrategy,
LovelaceStrategyParams,
LovelaceViewStrategy,
} from "./types";
const MAX_WAIT_STRATEGY_LOAD = 5000; const MAX_WAIT_STRATEGY_LOAD = 5000;
const CUSTOM_PREFIX = "custom:"; const CUSTOM_PREFIX = "custom:";
export interface LovelaceDashboardStrategy { const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
generateDashboard(info: { dashboard: {
config?: LovelaceConfig; "original-states": () => import("./original-states-dashboard-strategy"),
hass: HomeAssistant; },
narrow: boolean | undefined; view: {
}): Promise<LovelaceConfig>; "original-states": () => import("./original-states-view-strategy"),
} energy: () => import("../../energy/strategies/energy-view-strategy"),
},
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 getLovelaceStrategy = async < export type LovelaceStrategyConfigType = "dashboard" | "view";
T extends LovelaceDashboardStrategy | LovelaceViewStrategy,
>( 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 strategyType: string
): Promise<T> => { ): Promise<LovelaceStrategy> => {
if (strategyType in strategies) { if (strategyType in STRATEGIES[configType]) {
return (await strategies[strategyType]()) as T; await STRATEGIES[configType][strategyType]();
const tag = `${strategyType}-${configType}-strategy`;
return customElements.get(tag) as unknown as Strategies[T];
} }
if (!strategyType.startsWith(CUSTOM_PREFIX)) { if (!strategyType.startsWith(CUSTOM_PREFIX)) {
throw new Error("Unknown strategy"); 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 ( if (
(await Promise.race([ (await Promise.race([
customElements.whenDefined(legacyTag),
customElements.whenDefined(tag), customElements.whenDefined(tag),
new Promise((resolve) => { new Promise((resolve) => {
setTimeout(() => resolve(true), MAX_WAIT_STRATEGY_LOAD); 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 { const generateStrategy = async <T extends LovelaceStrategyConfigType>(
generateDashboard: LovelaceDashboardStrategy["generateDashboard"]; configType: T,
generateView: LovelaceViewStrategy["generateView"]; renderError: (err: string | Error) => StrategyConfig<T>,
} strategyConfig: LovelaceStrategyConfig,
hass: HomeAssistant,
const generateStrategy = async <T extends keyof GenerateMethods>( params: LovelaceStrategyParams
generateMethod: T, ): Promise<StrategyConfig<T>> => {
renderError: (err: string | Error) => AsyncReturnType<GenerateMethods[T]>, const strategyType = strategyConfig.type;
info: Parameters<GenerateMethods[T]>[0],
strategyType: string | undefined
): Promise<ReturnType<GenerateMethods[T]>> => {
if (!strategyType) { if (!strategyType) {
// @ts-ignore // @ts-ignore
return renderError("No strategy type found"); return renderError("No strategy type found");
} }
try { try {
const strategy = (await getLovelaceStrategy(strategyType)) as any; const strategy = await getLovelaceStrategy<T>(configType, strategyType);
// eslint-disable-next-line @typescript-eslint/return-await
return await strategy[generateMethod](info); // 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) { } catch (err: any) {
if (err.message !== "timeout") { if (err.message !== "timeout") {
// eslint-disable-next-line // eslint-disable-next-line
@ -93,11 +127,12 @@ const generateStrategy = async <T extends keyof GenerateMethods>(
}; };
export const generateLovelaceDashboardStrategy = async ( export const generateLovelaceDashboardStrategy = async (
info: Parameters<LovelaceDashboardStrategy["generateDashboard"]>[0], strategyConfig: LovelaceStrategyConfig,
strategyType?: string hass: HomeAssistant,
): ReturnType<LovelaceDashboardStrategy["generateDashboard"]> => params: LovelaceStrategyParams
): Promise<LovelaceConfig> =>
generateStrategy( generateStrategy(
"generateDashboard", "dashboard",
(err) => ({ (err) => ({
views: [ views: [
{ {
@ -111,16 +146,18 @@ export const generateLovelaceDashboardStrategy = async (
}, },
], ],
}), }),
info, strategyConfig,
strategyType || info.config?.strategy?.type hass,
params
); );
export const generateLovelaceViewStrategy = async ( export const generateLovelaceViewStrategy = async (
info: Parameters<LovelaceViewStrategy["generateView"]>[0], strategyConfig: LovelaceStrategyConfig,
strategyType?: string hass: HomeAssistant,
): ReturnType<LovelaceViewStrategy["generateView"]> => params: LovelaceStrategyParams
): Promise<LovelaceViewConfig> =>
generateStrategy( generateStrategy(
"generateView", "view",
(err) => ({ (err) => ({
cards: [ cards: [
{ {
@ -129,34 +166,30 @@ export const generateLovelaceViewStrategy = async (
}, },
], ],
}), }),
info, strategyConfig,
strategyType || info.view?.strategy?.type hass,
params
); );
/** /**
* Find all references to strategies and replaces them with the generated output * Find all references to strategies and replaces them with the generated output
*/ */
export const expandLovelaceConfigStrategies = async ( export const expandLovelaceConfigStrategies = async (
info: Parameters<LovelaceDashboardStrategy["generateDashboard"]>[0] & { config: LovelaceConfig,
config: LovelaceConfig; hass: HomeAssistant,
} params: LovelaceStrategyParams
): Promise<LovelaceConfig> => { ): Promise<LovelaceConfig> => {
const config = info.config.strategy const newConfig = config.strategy
? await generateLovelaceDashboardStrategy(info) ? await generateLovelaceDashboardStrategy(config.strategy, hass, params)
: { ...info.config }; : { ...config };
config.views = await Promise.all( newConfig.views = await Promise.all(
config.views.map((view) => newConfig.views.map((view) =>
view.strategy view.strategy
? generateLovelaceViewStrategy({ ? generateLovelaceViewStrategy(view.strategy, hass, params)
hass: info.hass,
narrow: info.narrow,
config,
view,
})
: view : 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 { 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 { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { getEnergyPreferences } from "../../../data/energy"; import { getEnergyPreferences } from "../../../data/energy";
import { generateDefaultViewConfig } from "../common/generate-lovelace-config";
import { import {
LovelaceDashboardStrategy, LovelaceStrategyConfig,
LovelaceViewStrategy, LovelaceViewConfig,
} from "./get-strategy"; } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
export class OriginalStatesStrategy { import { generateDefaultViewConfig } from "../common/generate-lovelace-config";
static async generateView( import { LovelaceStrategyParams } from "./types";
info: Parameters<LovelaceViewStrategy["generateView"]>[0]
): ReturnType<LovelaceViewStrategy["generateView"]> {
const hass = info.hass;
@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) { if (hass.config.state === STATE_NOT_RUNNING) {
return { return {
cards: [{ type: "starting" }], cards: [{ type: "starting" }],
@ -63,17 +68,4 @@ export class OriginalStatesStrategy {
return view; 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) { if (viewConfig.strategy) {
isStrategy = true; isStrategy = true;
viewConfig = await generateLovelaceViewStrategy({ viewConfig = await generateLovelaceViewStrategy(
hass: this.hass, viewConfig.strategy,
config: this.lovelace.config, this.hass!,
narrow: this.narrow, { narrow: this.narrow }
view: viewConfig, );
});
} }
viewConfig = { viewConfig = {