Compare commits

...

5 Commits

Author SHA1 Message Date
Paul Bottein
9fada7ceec Fix rebase 2026-03-24 18:01:30 +01:00
Paul Bottein
ea5c4817d2 Use checkStrategyShouldRegenerate in views, sections, and panels
Replace hardcoded registry checks with checkStrategyShouldRegenerate
in hui-view, hui-section, ha-panel-lovelace, and ha-panel-home.
Regeneration checks are also extended to sections (previously only
dashboards and views were checked).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 17:59:49 +01:00
Paul Bottein
8aaf793fe0 Add registryDependencies to all built-in strategies
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 17:59:49 +01:00
Paul Bottein
2ad5c808ca Add registryDependencies and checkStrategyShouldRegenerate to strategy interface
Strategies can now declare which registries they depend on via a static
`registryDependencies` property. A `shouldRegenerate` method can also
be implemented for custom logic. The `checkStrategyShouldRegenerate`
helper uses these to decide whether regeneration is needed, falling
back to checking entities, devices, areas, and floors by default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 17:59:49 +01:00
Paul Bottein
ea73e821ba Refactor lovelace view lifecycle to avoid unnecessary DOM rebuilds
- Remove `force` flag from `hui-root` that was clearing the entire view
  cache and destroying all cached view DOM on any config change. Views
  now receive updated lovelace in place and handle config changes
  internally.
- Add `_cleanupViewCache` to remove stale cache entries when views are
  added, removed, or reordered.
- Remove `@ll-rebuild` handler from `hui-root`. Cards and badges already
  handle `ll-rebuild` via their `hui-card`/`hui-badge` wrappers. Sections
  now always stop propagation and rebuild locally.
- Add `deepEqual` guard in `hui-view._setConfig` and
  `hui-section._initializeConfig` to skip re-rendering when strategy
  regeneration produces an identical config.
- Simplify `hui-view` refresh flow: remove `_refreshConfig`,
  `_rendered` flag, `strategy-config-changed` event, and
  connected/disconnected callbacks. Registry changes now debounce
  directly into `_initializeConfig`.
- Fix `isStrategy` check in `hui-view._initializeConfig` to use the raw
  config (before strategy expansion) rather than the generated config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 17:59:46 +01:00
22 changed files with 182 additions and 94 deletions

View File

@@ -9,6 +9,8 @@ import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
@customElement("energy-overview-view-strategy")
export class EnergyOverviewViewStrategy extends ReactiveElement {
static registryDependencies = [];
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant

View File

@@ -15,6 +15,8 @@ import {
@customElement("energy-view-strategy")
export class EnergyViewStrategy extends ReactiveElement {
static registryDependencies = [];
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant

View File

@@ -9,6 +9,8 @@ import type { LovelaceSectionConfig } from "../../../data/lovelace/config/sectio
@customElement("gas-view-strategy")
export class GasViewStrategy extends ReactiveElement {
static registryDependencies = [];
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant

View File

@@ -11,6 +11,8 @@ import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
@customElement("power-view-strategy")
export class PowerViewStrategy extends ReactiveElement {
static registryDependencies = [];
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant

View File

@@ -10,6 +10,8 @@ import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
@customElement("water-view-strategy")
export class WaterViewStrategy extends ReactiveElement {
static registryDependencies = [];
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant

View File

@@ -7,7 +7,6 @@ import { styleMap } from "lit/directives/style-map";
import { atLeastVersion } from "../../common/config/version";
import { navigate } from "../../common/navigate";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-button";
import "../../components/ha-svg-icon";
import { updateAreaRegistryEntry } from "../../data/area/area_registry";
@@ -26,7 +25,10 @@ import { showDeviceRegistryDetailDialog } from "../config/devices/device-registr
import { showAddIntegrationDialog } from "../config/integrations/show-add-integration-dialog";
import "../lovelace/hui-root";
import type { ExtraActionItem } from "../lovelace/hui-root";
import { expandLovelaceConfigStrategies } from "../lovelace/strategies/get-strategy";
import {
checkStrategyShouldRegenerate,
generateLovelaceDashboardStrategy,
} from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home";
import { showNewOverviewDialog } from "./dialogs/show-dialog-new-overview";
@@ -95,26 +97,19 @@ class PanelHome extends LitElement {
return;
}
if (oldHass && this.hass) {
// If the entity registry changed, ask the user if they want to refresh the config
if (
oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors ||
oldHass.panels !== this.hass.panels
) {
if (this.hass.config.state === "RUNNING") {
this._debounceRegistriesChanged();
return;
}
}
// If ha started, refresh the config
if (
this.hass.config.state === "RUNNING" &&
oldHass.config.state !== "RUNNING"
) {
if (oldHass && this.hass && this.hass.config.state === "RUNNING") {
if (oldHass.config.state !== "RUNNING") {
this._setup();
return;
}
const shouldRegenerate = checkStrategyShouldRegenerate(
"dashboard",
this._strategyConfig.strategy,
oldHass,
this.hass
);
if (shouldRegenerate) {
this._debounceRegenerateStrategy();
}
}
}
@@ -135,12 +130,12 @@ class PanelHome extends LitElement {
this._setLovelace();
}
private _debounceRegistriesChanged = debounce(
() => this._registriesChanged(),
private _debounceRegenerateStrategy = debounce(
() => this._regenerateStrategyConfig(),
200
);
private _registriesChanged = async () => {
private _regenerateStrategyConfig() {
// If on an area view that no longer exists, redirect to overview
const path = this.route?.path?.split("/")[1];
if (path?.startsWith("areas-")) {
@@ -151,7 +146,7 @@ class PanelHome extends LitElement {
}
}
this._setLovelace();
};
}
private _updateExtraActionItems() {
const path = this.route?.path?.split("/")[1];
@@ -312,24 +307,22 @@ class PanelHome extends LitElement {
});
}
private async _setLovelace() {
const strategyConfig: LovelaceDashboardStrategyConfig = {
private get _strategyConfig(): LovelaceDashboardStrategyConfig {
return {
strategy: {
type: "home",
favorite_entities: this._config.favorite_entities,
home_panel: true,
},
};
}
const config = await expandLovelaceConfigStrategies(
strategyConfig,
private async _setLovelace() {
const config = await generateLovelaceDashboardStrategy(
this._strategyConfig,
this.hass
);
if (deepEqual(config, this._lovelace?.config)) {
return;
}
this._lovelace = {
config: config,
rawConfig: config,

View File

@@ -12,7 +12,6 @@ import {
removeSearchParam,
} from "../../common/url/search-params";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-button";
import { domainToName } from "../../data/integration";
import { subscribeLovelaceUpdates } from "../../data/lovelace";
@@ -36,7 +35,10 @@ import { checkLovelaceConfig } from "./common/check-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 {
checkStrategyShouldRegenerate,
generateLovelaceDashboardStrategy,
} from "./strategies/get-strategy";
import type { Lovelace } from "./types";
import { generateDefaultView } from "./views/default-view";
import { fetchDashboards } from "../../data/lovelace/dashboard";
@@ -50,12 +52,6 @@ interface LovelacePanelConfig {
let editorLoaded = false;
let resourcesLoaded = false;
declare global {
interface HASSDomEvents {
"strategy-config-changed": undefined;
}
}
@customElement("ha-panel-lovelace")
export class LovelacePanel extends LitElement {
@property({ attribute: false }) public panel?: PanelInfo<
@@ -129,7 +125,6 @@ export class LovelacePanel extends LitElement {
.route=${this.route}
.narrow=${this.narrow}
@config-refresh=${this._forceFetchConfig}
@strategy-config-changed=${this._strategyConfigChanged}
></hui-root>
`;
}
@@ -195,61 +190,26 @@ export class LovelacePanel extends LitElement {
this.lovelace &&
isStrategyDashboard(this.lovelace.rawConfig)
) {
// If the entity registry changed, ask the user if they want to refresh the config
if (
oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors
) {
if (this.hass.config.state === "RUNNING") {
this._debounceRegistriesChanged();
}
}
// If ha started, refresh the config
if (
this.hass.config.state === "RUNNING" &&
oldHass.config.state !== "RUNNING"
(oldHass.config.state !== "RUNNING" ||
checkStrategyShouldRegenerate(
"dashboard",
this.lovelace.rawConfig.strategy,
oldHass,
this.hass
))
) {
this._regenerateStrategyConfig();
this._debounceRegenerateStrategy();
}
}
}
private _debounceRegistriesChanged = debounce(
() => this._registriesChanged(),
private _debounceRegenerateStrategy = debounce(
() => this._regenerateStrategyConfig(),
200
);
private _registriesChanged = async () => {
if (!this.hass || !this.lovelace) {
return;
}
const rawConfig = this.lovelace.rawConfig;
if (!isStrategyDashboard(rawConfig)) {
return;
}
const oldConfig = this.lovelace.config;
const generatedConfig = await generateLovelaceDashboardStrategy(
rawConfig,
this.hass!
);
const newConfig = checkLovelaceConfig(generatedConfig) as LovelaceConfig;
// Regenerate if the config changed
if (!deepEqual(newConfig, oldConfig)) {
this._regenerateStrategyConfig();
}
};
private _strategyConfigChanged = (ev: CustomEvent) => {
ev.stopPropagation();
this._regenerateStrategyConfig();
};
private async _regenerateStrategyConfig() {
if (!this.hass || !this.lovelace) {
return;

View File

@@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import { storage } from "../../../common/decorators/storage";
import { deepEqual } from "../../../common/util/deep-equal";
import { fireEvent } from "../../../common/dom/fire_event";
import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-svg-icon";
import type { LovelaceSectionElement } from "../../../data/lovelace";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
@@ -24,7 +25,10 @@ import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"
import { addCard, replaceCard } from "../editor/config-util";
import { performDeleteCard } from "../editor/delete-card";
import { parseLovelaceCardPath } from "../editor/lovelace-path";
import { generateLovelaceSectionStrategy } from "../strategies/get-strategy";
import {
checkStrategyShouldRegenerate,
generateLovelaceSectionStrategy,
} from "../strategies/get-strategy";
import type { Lovelace } from "../types";
import { DEFAULT_SECTION_LAYOUT } from "./const";
@@ -105,9 +109,36 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
(!oldConfig || this.config !== oldConfig)
) {
this._initializeConfig();
return;
}
if (!changedProperties.has("hass")) {
return;
}
const oldHass = changedProperties.get("hass") as HomeAssistant | undefined;
if (
oldHass &&
this.hass &&
isStrategySection(this.config) &&
this.hass.config.state === "RUNNING" &&
(oldHass.config.state !== "RUNNING" ||
checkStrategyShouldRegenerate(
"section",
this.config.strategy,
oldHass,
this.hass
))
) {
this._debounceRefreshConfig();
}
}
private _debounceRefreshConfig = debounce(
() => this._initializeConfig(),
200
);
public disconnectedCallback() {
super.disconnectedCallback();
}

View File

@@ -34,6 +34,8 @@ const computeHeadingCard = (
@customElement("area-view-strategy")
export class AreaViewStrategy extends ReactiveElement {
static registryDependencies = ["entities", "devices", "areas"];
static async generate(
config: AreaViewStrategyConfig,
hass: HomeAssistant

View File

@@ -31,6 +31,8 @@ export interface AreasDashboardStrategyConfig {
@customElement("areas-dashboard-strategy")
export class AreasDashboardStrategy extends ReactiveElement {
static registryDependencies = ["areas"];
static async generate(
config: AreasDashboardStrategyConfig,
hass: HomeAssistant

View File

@@ -19,6 +19,7 @@ import type { AsyncReturnType, HomeAssistant } from "../../../types";
import { cleanLegacyStrategyConfig, isLegacyStrategy } from "./legacy-strategy";
import type {
LovelaceDashboardStrategy,
LovelaceStrategyRegistryKey,
LovelaceSectionStrategy,
LovelaceStrategy,
LovelaceViewStrategy,
@@ -27,6 +28,13 @@ import type {
const MAX_WAIT_STRATEGY_LOAD = 5000;
const CUSTOM_PREFIX = "custom:";
const DEFAULT_REGISTRY_DEPENDENCIES: readonly LovelaceStrategyRegistryKey[] = [
"entities",
"devices",
"areas",
"floors",
];
const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
dashboard: {
"original-states": () =>
@@ -237,6 +245,45 @@ export const generateLovelaceSectionStrategy = async (
};
};
/**
* Synchronously checks whether a strategy needs regeneration.
* Falls back to checking common registries if the strategy doesn't implement shouldRegenerate.
*/
export const checkStrategyShouldRegenerate = (
configType: LovelaceStrategyConfigType,
strategyConfig: LovelaceStrategyConfig,
oldHass: HomeAssistant,
newHass: HomeAssistant
): boolean => {
const strategyType = strategyConfig.type;
if (!strategyType) {
return false;
}
let strategy: LovelaceStrategy | undefined;
if (strategyType in STRATEGIES[configType]) {
const tag = `${strategyType}-${configType}-strategy`;
strategy = customElements.get(tag) as unknown as
| LovelaceStrategy
| undefined;
} else if (strategyType.startsWith(CUSTOM_PREFIX)) {
const name = strategyType.slice(CUSTOM_PREFIX.length);
const tag = `ll-strategy-${configType}-${name}`;
const legacyTag = `ll-strategy-${name}`;
strategy = (customElements.get(tag) ??
customElements.get(legacyTag)) as unknown as LovelaceStrategy | undefined;
}
if (strategy?.shouldRegenerate) {
return strategy.shouldRegenerate(strategyConfig, oldHass, newHass);
}
const dependencies =
strategy?.registryDependencies ?? DEFAULT_REGISTRY_DEPENDENCIES;
return dependencies.some((key) => oldHass[key] !== newHass[key]);
};
/**
* Find all references to strategies and replaces them with the generated output
*/

View File

@@ -32,6 +32,8 @@ export interface HomeAreaViewStrategyConfig {
@customElement("home-area-view-strategy")
export class HomeAreaViewStrategy extends ReactiveElement {
static registryDependencies = ["entities", "devices", "areas", "panels"];
static async generate(
config: HomeAreaViewStrategyConfig,
hass: HomeAssistant

View File

@@ -21,6 +21,8 @@ export interface HomeDashboardStrategyConfig {
@customElement("home-dashboard-strategy")
export class HomeDashboardStrategy extends ReactiveElement {
static registryDependencies = ["areas"];
static async generate(
config: HomeDashboardStrategyConfig,
hass: HomeAssistant

View File

@@ -72,6 +72,14 @@ const computeAreaCard = (
@customElement("home-overview-view-strategy")
export class HomeOverviewViewStrategy extends ReactiveElement {
static registryDependencies = [
"entities",
"devices",
"areas",
"floors",
"panels",
];
static async generate(
config: HomeOverviewViewStrategyConfig,
hass: HomeAssistant

View File

@@ -8,6 +8,8 @@ export type IframeDashboardStrategyConfig = IframeViewStrategyConfig;
@customElement("iframe-dashboard-strategy")
export class IframeDashboardStrategy extends ReactiveElement {
static registryDependencies = [];
static async generate(
config: IframeDashboardStrategyConfig
): Promise<LovelaceConfig> {

View File

@@ -11,6 +11,8 @@ export interface IframeViewStrategyConfig {
@customElement("iframe-view-strategy")
export class IframeViewStrategy extends ReactiveElement {
static registryDependencies = [];
static async generate(
config: IframeViewStrategyConfig
): Promise<LovelaceViewConfig> {

View File

@@ -7,6 +7,8 @@ export type MapDashboardStrategyConfig = MapViewStrategyConfig;
@customElement("map-dashboard-strategy")
export class MapDashboardStrategy extends ReactiveElement {
static registryDependencies = [];
static async generate(
config: MapDashboardStrategyConfig
): Promise<LovelaceConfig> {

View File

@@ -10,6 +10,8 @@ export interface MapViewStrategyConfig {
@customElement("map-view-strategy")
export class MapViewStrategy extends ReactiveElement {
static registryDependencies = [];
static async generate(
_config: MapViewStrategyConfig,
hass: HomeAssistant

View File

@@ -9,6 +9,8 @@ export type OriginalStatesDashboardStrategyConfig =
@customElement("original-states-dashboard-strategy")
export class OriginalStatesDashboardStrategy extends ReactiveElement {
static registryDependencies = [];
static async generate(
config: OriginalStatesDashboardStrategyConfig
): Promise<LovelaceConfig> {

View File

@@ -5,8 +5,22 @@ import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import type { LovelaceGenericElementEditor } from "../types";
export type LovelaceStrategyRegistryKey =
| "entities"
| "devices"
| "areas"
| "floors"
| "labels"
| "panels";
export interface LovelaceStrategy<T = any> {
generate(config: LovelaceStrategyConfig, hass: HomeAssistant): Promise<T>;
shouldRegenerate?(
config: LovelaceStrategyConfig,
oldHass: HomeAssistant,
newHass: HomeAssistant
): boolean;
registryDependencies?: readonly LovelaceStrategyRegistryKey[];
getConfigElement?: () => LovelaceStrategyEditor;
noEditor?: boolean;
configRequired?: boolean;

View File

@@ -26,6 +26,8 @@ export interface CommonControlSectionStrategyConfig {
@customElement("common-controls-section-strategy")
export class CommonControlsSectionStrategy extends ReactiveElement {
static registryDependencies = [];
static async generate(
config: CommonControlSectionStrategyConfig,
hass: HomeAssistant

View File

@@ -5,7 +5,6 @@ import { customElement, property, state } from "lit/decorators";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { debounce } from "../../../common/util/debounce";
import { deepEqual } from "../../../common/util/deep-equal";
import "../../../components/entity/ha-state-label-badge";
import "../../../components/ha-svg-icon";
import type { LovelaceViewElement } from "../../../data/lovelace";
@@ -42,7 +41,10 @@ import { parseLovelaceCardPath } from "../editor/lovelace-path";
import { createErrorSectionConfig } from "../sections/hui-error-section";
import "../sections/hui-section";
import type { HuiSection } from "../sections/hui-section";
import { generateLovelaceViewStrategy } from "../strategies/get-strategy";
import {
checkStrategyShouldRegenerate,
generateLovelaceViewStrategy,
} from "../strategies/get-strategy";
import type { Lovelace } from "../types";
import { getViewType } from "./get-view-type";
@@ -185,10 +187,13 @@ export class HUIView extends ReactiveElement {
if (oldHass && this.hass && this.lovelace && isStrategyView(viewConfig)) {
if (
this.hass.config.state === "RUNNING" &&
(oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors)
(oldHass.config.state !== "RUNNING" ||
checkStrategyShouldRegenerate(
"view",
viewConfig.strategy,
oldHass,
this.hass
))
) {
this._debounceRefreshConfig();
}