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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user