1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-02 08:33:31 +01:00

Support Energy Collections in Statistic Card Visual Editor (#29629)

* Improve energy support for statistics card

Rather than setting the period to "energy_date_selection", add an energy_date_selection key to the config to be more consistent. The original configuration option is still supported for backwards compatibility

Update the statistic card visual editor to support energy collection key selection.

* Add statistics card collection key validation

* Apply suggestions from code review

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Remove title from EnergyCardBaseConfig

This was added when the energy card visual editors were created, but not all cards using a collection key need a title. Using Omit to remove it seems to lose the extend of LovelaceCardConfig.

Instead add EnergyCardConfig which is EnergyCardBaseConfig with the title field. This is used for a number of cards to allow them to share the same visual editor without having to list out every one.

* Mark Statistic Period calendar.offset as optional

It is already handled in core as an optional key (defaults to 0), and the statistic card/editor was explicitly omiting the key even though it was declaring it as required.

This removes the need for an error masking cast when converting the deprecated PERIOD_ENERGY to STATISTIC_CARD_DEFAULT_PERIOD.

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
Tom Carpenter
2026-03-13 10:47:37 +00:00
committed by GitHub
parent e8e62a0aa0
commit fa5e0bb0c7
6 changed files with 166 additions and 70 deletions

View File

@@ -195,7 +195,7 @@ export const fetchStatistic = (
statistic_id: string,
period: {
fixed_period?: { start: string | Date; end: string | Date };
calendar?: { period: string; offset: number };
calendar?: { period: string; offset?: number };
rolling_window?: { duration: HaDurationData; offset: HaDurationData };
},
units?: StatisticsUnitConfiguration

View File

@@ -9,7 +9,10 @@ import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-state-icon";
import { getEnergyDataCollection } from "../../../data/energy";
import {
getEnergyDataCollection,
validateEnergyCollectionKey,
} from "../../../data/energy";
import type { StatisticsMetaData } from "../../../data/recorder";
import {
fetchStatistic,
@@ -33,7 +36,11 @@ import type {
import type { HuiErrorCard } from "./hui-error-card";
import type { EntityCardConfig, StatisticCardConfig } from "./types";
/* @deprecated */
export const PERIOD_ENERGY = "energy_date_selection";
export const STATISTIC_CARD_DEFAULT_PERIOD = {
calendar: { period: "month" },
};
@customElement("hui-statistic-card")
export class HuiStatisticCard extends LitElement implements LovelaceCard {
@@ -60,7 +67,7 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard {
return {
entity: foundEntities[0] || "",
period: { calendar: { period: "month" } },
period: STATISTIC_CARD_DEFAULT_PERIOD,
};
}
@@ -92,7 +99,7 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard {
public connectedCallback() {
super.connectedCallback();
if (this._config?.period === PERIOD_ENERGY) {
if (this._useEnergyDateSelect()) {
this._subscribeEnergy();
} else {
this._setFetchStatisticTimer();
@@ -150,6 +157,17 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard {
) {
throw new Error("Invalid entity");
}
if (config.collection_key) {
validateEnergyCollectionKey(config.collection_key);
}
// Migrate legacy period option to new key
if (config.period === PERIOD_ENERGY) {
config = {
energy_date_selection: true,
...config,
period: STATISTIC_CARD_DEFAULT_PERIOD,
};
}
this._config = config;
this._error = undefined;
@@ -250,17 +268,18 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard {
| undefined;
if (this.hass) {
if (this._config.period === PERIOD_ENERGY && !this._energySub) {
const useDateSelect = this._useEnergyDateSelect();
if (useDateSelect && !this._energySub) {
this._subscribeEnergy();
return;
}
if (this._config.period !== PERIOD_ENERGY && this._energySub) {
if (!useDateSelect && this._energySub) {
this._unsubscribeEnergy();
this._setFetchStatisticTimer();
return;
}
if (
this._config.period === PERIOD_ENERGY &&
useDateSelect &&
this._energySub &&
changedProps.has("_config") &&
oldConfig?.collection_key !== this._config.collection_key
@@ -306,11 +325,19 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard {
}
}
private _useEnergyDateSelect() {
if (!this._config) return false;
// Use date selection if enabled through config key
if (this._config.energy_date_selection) return true;
// Otherwise check if period key is set to the legacy energy mode value
return this._config.period === PERIOD_ENERGY;
}
private _setFetchStatisticTimer() {
this._fetchStatistic();
// statistics are created every hour
clearInterval(this._interval);
if (this._config?.period !== PERIOD_ENERGY) {
if (!this._useEnergyDateSelect()) {
this._interval = window.setInterval(
() => this._fetchStatistic(),
5 * 1000 * 60

View File

@@ -166,11 +166,14 @@ export interface ButtonCardConfig extends LovelaceCardConfig {
}
export interface EnergyCardBaseConfig extends LovelaceCardConfig {
title?: string;
collection_key?: string;
}
export interface EnergyCardSankeyConfig extends EnergyCardBaseConfig {
export interface EnergyCardConfig extends EnergyCardBaseConfig {
title?: string;
}
export interface EnergyCardSankeyConfig extends EnergyCardConfig {
layout?: "auto" | "vertical" | "horizontal";
group_by_floor?: boolean;
group_by_area?: boolean;
@@ -182,61 +185,61 @@ export interface EnergyDateSelectorCardConfig extends EnergyCardBaseConfig {
disable_compare?: boolean;
}
export interface EnergyDistributionCardConfig extends EnergyCardBaseConfig {
export interface EnergyDistributionCardConfig extends EnergyCardConfig {
type: "energy-distribution";
link_dashboard?: boolean;
}
export interface EnergyUsageGraphCardConfig extends EnergyCardBaseConfig {
export interface EnergyUsageGraphCardConfig extends EnergyCardConfig {
type: "energy-usage-graph";
}
export interface EnergySolarGraphCardConfig extends EnergyCardBaseConfig {
export interface EnergySolarGraphCardConfig extends EnergyCardConfig {
type: "energy-solar-graph";
}
export interface EnergyGasGraphCardConfig extends EnergyCardBaseConfig {
export interface EnergyGasGraphCardConfig extends EnergyCardConfig {
type: "energy-gas-graph";
}
export interface EnergyWaterGraphCardConfig extends EnergyCardBaseConfig {
export interface EnergyWaterGraphCardConfig extends EnergyCardConfig {
type: "energy-water-graph";
}
export interface EnergyDevicesGraphCardConfig extends EnergyCardBaseConfig {
export interface EnergyDevicesGraphCardConfig extends EnergyCardConfig {
type: "energy-devices-graph";
max_devices?: number;
hide_compound_stats?: boolean;
modes?: ("bar" | "pie")[];
}
export interface EnergyDevicesDetailGraphCardConfig extends EnergyCardBaseConfig {
export interface EnergyDevicesDetailGraphCardConfig extends EnergyCardConfig {
type: "energy-devices-detail-graph";
max_devices?: number;
}
export interface EnergySourcesTableCardConfig extends EnergyCardBaseConfig {
export interface EnergySourcesTableCardConfig extends EnergyCardConfig {
type: "energy-sources-table";
types?: (keyof EnergySourceByType)[];
show_only_totals?: boolean;
}
export interface EnergySolarGaugeCardConfig extends EnergyCardBaseConfig {
export interface EnergySolarGaugeCardConfig extends EnergyCardConfig {
type: "energy-solar-consumed-gauge";
}
export interface EnergySelfSufficiencyGaugeCardConfig extends EnergyCardBaseConfig {
export interface EnergySelfSufficiencyGaugeCardConfig extends EnergyCardConfig {
type: "energy-self-sufficiency-gauge";
}
export interface EnergyGridNeutralityGaugeCardConfig extends EnergyCardBaseConfig {
export interface EnergyGridNeutralityGaugeCardConfig extends EnergyCardConfig {
type: "energy-grid-neutrality-gauge";
}
export interface EnergyCarbonGaugeCardConfig extends EnergyCardBaseConfig {
export interface EnergyCarbonGaugeCardConfig extends EnergyCardConfig {
type: "energy-carbon-consumed-gauge";
}
export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig {
export interface PowerSourcesGraphCardConfig extends EnergyCardConfig {
type: "power-sources-graph";
show_legend?: boolean;
}
@@ -471,16 +474,17 @@ export interface StatisticsGraphCardConfig extends EnergyCardBaseConfig {
expand_legend?: boolean;
}
export interface StatisticCardConfig extends LovelaceCardConfig {
export interface StatisticCardConfig extends EnergyCardBaseConfig {
name?: string | EntityNameItem | EntityNameItem[];
entities: (EntityConfig | string)[];
period:
| {
fixed_period?: { start: string; end: string };
calendar?: { period: string; offset: number };
calendar?: { period: string; offset?: number };
rolling_window?: { duration: HaDurationData; offset: HaDurationData };
}
| "energy_date_selection";
| "energy_date_selection"; // Maintained for legacy compatibility, use new key instead.
energy_date_selection?: boolean;
stat_type: keyof Statistic;
theme?: string;
}

View File

@@ -19,7 +19,7 @@ import "../../../../components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type {
EnergyCardBaseConfig,
EnergyCardConfig,
EnergyDevicesDetailGraphCardConfig,
EnergyDevicesGraphCardConfig,
} from "../../cards/types";
@@ -44,7 +44,7 @@ const cardConfigStruct = assign(
const chartModeOpts = ["bar", "pie"] as const;
type EnergyDevicesCardConfig =
| EnergyCardBaseConfig
| EnergyCardConfig
| EnergyDevicesGraphCardConfig
| EnergyDevicesDetailGraphCardConfig;
@customElement("hui-energy-devices-card-editor")

View File

@@ -17,6 +17,7 @@ import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type {
EnergyCardBaseConfig,
EnergyCardConfig,
EnergyDistributionCardConfig,
PowerSourcesGraphCardConfig,
} from "../../cards/types";
@@ -46,10 +47,6 @@ const cardConfigStruct = assign(
})
);
type EnergyGraphCardConfig =
| EnergyCardBaseConfig
| EnergyDistributionCardConfig
| PowerSourcesGraphCardConfig;
@customElement("hui-energy-graph-card-editor")
export class HuiEnergyGraphCardEditor
extends LitElement
@@ -57,16 +54,28 @@ export class HuiEnergyGraphCardEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: EnergyGraphCardConfig;
@state() private _config?:
| EnergyCardBaseConfig
| EnergyCardConfig
| EnergyDistributionCardConfig
| PowerSourcesGraphCardConfig;
public setConfig(config: EnergyGraphCardConfig): void {
public setConfig(
config:
| EnergyCardBaseConfig
| EnergyCardConfig
| EnergyDistributionCardConfig
| PowerSourcesGraphCardConfig
): void {
assert(config, cardConfigStruct);
this._config = config;
}
private _schema = memoizeOne((type: string) => {
const schema: HaFormSchema[] = [
{ name: "title", selector: { text: {} } },
...(type !== "energy-compare"
? [{ name: "title", selector: { text: {} } }]
: []),
...(type === "power-sources-graph"
? [
{

View File

@@ -1,12 +1,20 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { any, assert, assign, object, optional, string } from "superstruct";
import {
any,
assert,
assign,
boolean,
object,
optional,
string,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { deepEqual } from "../../../../common/util/deep-equal";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type {
StatisticsMetaData,
StatisticType,
@@ -22,6 +30,10 @@ import { headerFooterConfigStructs } from "../../header-footer/structs";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import {
PERIOD_ENERGY,
STATISTIC_CARD_DEFAULT_PERIOD,
} from "../../cards/hui-statistic-card";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -34,6 +46,7 @@ const cardConfigStruct = assign(
period: optional(any()),
theme: optional(string()),
footer: optional(headerFooterConfigStructs),
energy_date_selection: optional(boolean()),
collection_key: optional(string()),
})
);
@@ -71,6 +84,14 @@ export class HuiStatisticCardEditor
public setConfig(config: StatisticCardConfig): void {
assert(config, cardConfigStruct);
// Migrate legacy period option to new key
if (config.period === PERIOD_ENERGY) {
config = {
energy_date_selection: true,
...config,
period: STATISTIC_CARD_DEFAULT_PERIOD,
};
}
this._config = config;
this._fetchMetadata();
}
@@ -104,6 +125,7 @@ export class HuiStatisticCardEditor
(
selectedPeriodKey: string | undefined,
localize: LocalizeFunc,
enableDateSelect: boolean,
metadata?: StatisticsMetaData
) =>
[
@@ -126,24 +148,48 @@ export class HuiStatisticCardEditor
},
},
},
...(!enableDateSelect
? [
{
name: "period",
required: true,
selector:
selectedPeriodKey && selectedPeriodKey in periods
? {
select: {
multiple: false,
options: Object.keys(periods).map((periodKey) => ({
value: periodKey,
label:
localize(
`ui.panel.lovelace.editor.card.statistic.periods.${periodKey}`
) || periodKey,
})),
},
}
: { object: {} },
},
]
: []),
{
name: "period",
required: true,
selector:
selectedPeriodKey && selectedPeriodKey in periods
? {
select: {
multiple: false,
options: Object.keys(periods).map((periodKey) => ({
value: periodKey,
label:
localize(
`ui.panel.lovelace.editor.card.statistic.periods.${periodKey}`
) || periodKey,
})),
name: "",
type: "grid",
schema: [
...(enableDateSelect
? ([
{
type: "string",
name: "collection_key",
required: false,
},
}
: { object: {} },
] as HaFormSchema[])
: []),
{
name: "energy_date_selection",
required: false,
selector: { boolean: {} },
},
],
},
{
name: "name",
@@ -180,6 +226,7 @@ export class HuiStatisticCardEditor
const schema = this._schema(
typeof data.period === "string" ? data.period : undefined,
this.hass.localize,
!!this._config!.energy_date_selection,
this._metadata
);
@@ -188,6 +235,7 @@ export class HuiStatisticCardEditor
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeHelper=${this._computeHelperCallback}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
@@ -244,26 +292,34 @@ export class HuiStatisticCardEditor
fireEvent(this, "config-changed", { config });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
if (schema.name === "period") {
return this.hass!.localize(
"ui.panel.lovelace.editor.card.statistic.period"
);
private _computeHelperCallback = (schema) => {
switch (schema.name) {
case "collection_key":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.collection_key_description`
);
default:
return undefined;
}
};
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
private _computeLabelCallback = (schema) => {
switch (schema.name) {
case "period":
return this.hass!.localize(
"ui.panel.lovelace.editor.card.statistic.period"
);
case "theme":
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
};
}