diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index d0c9265441..b0ee80b917 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -26,6 +26,7 @@ import { getStatisticMetadata, Statistics, statisticsHaveType, + StatisticsMetaData, StatisticType, } from "../../data/recorder"; import type { HomeAssistant } from "../../types"; @@ -46,6 +47,11 @@ class StatisticsChart extends LitElement { @property({ attribute: false }) public statisticsData!: Statistics; + @property({ attribute: false }) public metadata?: Record< + string, + StatisticsMetaData + >; + @property() public names: boolean | Record = false; @property() public unit?: string; @@ -76,7 +82,7 @@ class StatisticsChart extends LitElement { } public willUpdate(changedProps: PropertyValues) { - if (!this.hasUpdated) { + if (!this.hasUpdated || changedProps.has("unit")) { this._createOptions(); } if (changedProps.has("statisticsData") || changedProps.has("statTypes")) { @@ -120,7 +126,7 @@ class StatisticsChart extends LitElement { `; } - private _createOptions() { + private _createOptions(unit?: string) { this._chartOptions = { parsing: false, animation: false, @@ -154,8 +160,8 @@ class StatisticsChart extends LitElement { maxTicksLimit: 7, }, title: { - display: this.unit, - text: this.unit, + display: unit || this.unit, + text: unit || this.unit, }, }, }, @@ -220,9 +226,9 @@ class StatisticsChart extends LitElement { return; } - const statisticsMetaData = await this._getStatisticsMetaData( - Object.keys(this.statisticsData) - ); + const statisticsMetaData = + this.metadata || + (await this._getStatisticsMetaData(Object.keys(this.statisticsData))); let colorIndex = 0; const statisticsData = Object.values(this.statisticsData); @@ -263,6 +269,7 @@ class StatisticsChart extends LitElement { if (unit === undefined) { unit = getDisplayUnit(this.hass, firstStat.statistic_id, meta); } else if ( + unit !== null && unit !== getDisplayUnit(this.hass, firstStat.statistic_id, meta) ) { // Clear unit if not all statistics have same unit @@ -386,17 +393,8 @@ class StatisticsChart extends LitElement { Array.prototype.push.apply(totalDataSets, statDataSets); }); - if (unit !== null) { - this._chartOptions = { - ...this._chartOptions, - scales: { - ...this._chartOptions!.scales, - y: { - ...(this._chartOptions!.scales!.y as Record), - title: { display: unit, text: unit }, - }, - }, - }; + if (unit) { + this._createOptions(unit); } this._chartData = { diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index 140db7a27f..348f49494f 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -85,7 +85,7 @@ export class HaForm extends LitElement implements HaFormElement { .selector=${item.selector} .value=${getValue(this.data, item)} .label=${this._computeLabel(item, this.data)} - .disabled=${this.disabled} + .disabled=${this.disabled || item.disabled} .helper=${this._computeHelper(item)} .required=${item.required || false} .context=${this._generateContext(item)} @@ -95,7 +95,7 @@ export class HaForm extends LitElement implements HaFormElement { data: getValue(this.data, item), label: this._computeLabel(item, this.data), helper: this._computeHelper(item), - disabled: this.disabled, + disabled: this.disabled || item.disabled, hass: this.hass, computeLabel: this.computeLabel, computeHelper: this.computeHelper, diff --git a/src/components/ha-form/types.ts b/src/components/ha-form/types.ts index bce171a562..a41fad2d6e 100644 --- a/src/components/ha-form/types.ts +++ b/src/components/ha-form/types.ts @@ -20,6 +20,7 @@ export interface HaFormBaseSchema { // This value is applied if no data is submitted for this field default?: HaFormData; required?: boolean; + disabled?: boolean; description?: { suffix?: string; // This value will be set initially when form is loaded diff --git a/src/panels/lovelace/cards/hui-statistics-graph-card.ts b/src/panels/lovelace/cards/hui-statistics-graph-card.ts index 73fe66c3e2..0d26c38c3f 100644 --- a/src/panels/lovelace/cards/hui-statistics-graph-card.ts +++ b/src/panels/lovelace/cards/hui-statistics-graph-card.ts @@ -8,14 +8,20 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import "../../../components/ha-card"; import "../../../components/chart/statistics-chart"; +import "../../../components/ha-card"; +import { + fetchStatistics, + getDisplayUnit, + getStatisticMetadata, + Statistics, + StatisticsMetaData, +} from "../../../data/recorder"; import { HomeAssistant } from "../../../types"; import { hasConfigOrEntitiesChanged } from "../common/has-changed"; import { processConfigEntities } from "../common/process-config-entities"; import { LovelaceCard } from "../types"; import { StatisticsGraphCardConfig } from "./types"; -import { fetchStatistics, Statistics } from "../../../data/recorder"; @customElement("hui-statistics-graph-card") export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { @@ -30,9 +36,13 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { @property({ attribute: false }) public hass?: HomeAssistant; + @state() private _config?: StatisticsGraphCardConfig; + @state() private _statistics?: Statistics; - @state() private _config?: StatisticsGraphCardConfig; + @state() private _metadata?: Record; + + @state() private _unit?: string; private _entities: string[] = []; @@ -108,7 +118,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { public willUpdate(changedProps: PropertyValues) { super.willUpdate(changedProps); - if (!this._config || !changedProps.has("_config")) { + if ( + !this._config || + (!changedProps.has("_config") && !changedProps.has("_metadata")) + ) { return; } @@ -117,9 +130,19 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { | undefined; if ( - oldConfig?.entities !== this._config.entities || - oldConfig?.days_to_show !== this._config.days_to_show || - oldConfig?.period !== this._config.period + changedProps.has("_config") && + oldConfig?.entities !== this._config.entities + ) { + this._getStatisticsMetaData(this._entities); + } + + if ( + changedProps.has("_metadata") || + (changedProps.has("_config") && + (oldConfig?.entities !== this._config.entities || + oldConfig?.days_to_show !== this._config.days_to_show || + oldConfig?.period !== this._config.period || + oldConfig?.unit !== this._config.unit)) ) { this._getStatistics(); // statistics are created every hour @@ -147,9 +170,11 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { .hass=${this.hass} .isLoadingData=${!this._statistics} .statisticsData=${this._statistics} + .metadata=${this._metadata} .chartType=${this._config.chart_type || "line"} .statTypes=${this._config.stat_types!} .names=${this._names} + .unit=${this._unit} > @@ -160,6 +185,18 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { return (this._config?.period === "5minute" ? 5 : 60) * 1000 * 60; } + private async _getStatisticsMetaData(statisticIds: string[] | undefined) { + const statsMetadataArray = await getStatisticMetadata( + this.hass!, + statisticIds + ); + const statisticsMetaData = {}; + statsMetadataArray.forEach((x) => { + statisticsMetaData[x.statistic_id] = x; + }); + this._metadata = statisticsMetaData; + } + private async _getStatistics(): Promise { const startDate = new Date(); startDate.setTime( @@ -167,12 +204,34 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { 1000 * 60 * 60 * (24 * (this._config!.days_to_show || 30) + 1) ); try { + let unitClass; + if (this._config!.unit && this._metadata) { + const metadata = Object.values(this._metadata).find( + (metaData) => + getDisplayUnit(this.hass!, metaData?.statistic_id, metaData) === + this._config!.unit + ); + if (metadata) { + unitClass = metadata.unit_class; + this._unit = this._config!.unit; + } + } + if (!unitClass && this._metadata) { + const metadata = this._metadata[this._entities[0]]; + unitClass = metadata?.unit_class; + this._unit = unitClass + ? getDisplayUnit(this.hass!, metadata.statistic_id, metadata) || + undefined + : undefined; + } + const unitconfig = unitClass ? { [unitClass]: this._unit } : undefined; this._statistics = await fetchStatistics( this.hass!, startDate, undefined, this._entities, - this._config!.period + this._config!.period, + unitconfig ); } catch (err) { this._statistics = undefined; diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index fe8df228a0..75f1d2bdee 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -304,6 +304,7 @@ export interface HistoryGraphCardConfig extends LovelaceCardConfig { export interface StatisticsGraphCardConfig extends LovelaceCardConfig { title?: string; entities: Array; + unit?: string; days_to_show?: number; period?: "5minute" | "hour" | "day" | "month"; stat_types?: StatisticType | StatisticType[]; diff --git a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts index d8af7c848d..9a9e427601 100644 --- a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts @@ -1,4 +1,3 @@ -import "../../../../components/ha-form/ha-form"; import { css, CSSResultGroup, @@ -21,24 +20,26 @@ import { union, } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { ensureArray } from "../../../../common/ensure-array"; import type { LocalizeFunc } from "../../../../common/translations/localize"; +import { deepEqual } from "../../../../common/util/deep-equal"; +import { statTypeMap } from "../../../../components/chart/statistics-chart"; import "../../../../components/entity/ha-statistics-picker"; -import type { SchemaUnion } from "../../../../components/ha-form/types"; +import "../../../../components/ha-form/ha-form"; +import type { HaFormSchema } from "../../../../components/ha-form/types"; +import { + getDisplayUnit, + getStatisticMetadata, + isExternalStatistic, + StatisticsMetaData, + statisticsMetaHasType, +} from "../../../../data/recorder"; import type { HomeAssistant } from "../../../../types"; import type { StatisticsGraphCardConfig } from "../../cards/types"; import { processConfigEntities } from "../../common/process-config-entities"; import type { LovelaceCardEditor } from "../../types"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { entitiesConfigStruct } from "../structs/entities-struct"; -import { - getStatisticMetadata, - isExternalStatistic, - StatisticsMetaData, - statisticsMetaHasType, -} from "../../../../data/recorder"; -import { deepEqual } from "../../../../common/util/deep-equal"; -import { statTypeMap } from "../../../../components/chart/statistics-chart"; -import { ensureArray } from "../../../../common/ensure-array"; const statTypeStruct = union([ literal("state"), @@ -65,6 +66,7 @@ const cardConfigStruct = assign( ), chart_type: optional(union([literal("bar"), literal("line")])), stat_types: optional(union([array(statTypeStruct), statTypeStruct])), + unit: optional(string()), }) ); @@ -114,8 +116,19 @@ export class HuiStatisticsGraphCardEditor localize: LocalizeFunc, statisticIds: string[] | undefined, metaDatas: StatisticsMetaData[] | undefined - ) => - [ + ) => { + const units = new Set(); + metaDatas?.forEach((metaData) => { + const unit = getDisplayUnit( + this.hass!, + metaData.statistic_id, + metaData + ); + if (unit) { + units.add(unit); + } + }); + const schema: HaFormSchema[] = [ { name: "title", selector: { text: {} } }, { name: "", @@ -159,7 +172,7 @@ export class HuiStatisticsGraphCardEditor ), disabled: !metaDatas || - !metaDatas?.every((metaData) => + !metaDatas.every((metaData) => statisticsMetaHasType(metaData, statTypeMap[stat_type]) ), })), @@ -177,7 +190,25 @@ export class HuiStatisticsGraphCardEditor }, ], }, - ] as const + ]; + + if (units.size > 1) { + (schema[1] as any).schema.push({ + name: "unit", + required: false, + selector: { + select: { + options: Array.from(units).map((unit) => ({ + value: unit, + label: unit, + })), + }, + }, + }); + } + + return schema; + } ); protected render(): TemplateResult { @@ -207,6 +238,9 @@ export class HuiStatisticsGraphCardEditor stat_types: configured_stat_types, }; const unitClass = this._metaDatas?.[0]?.unit_class; + const statisticsUnit = unitClass + ? undefined + : this._metaDatas?.[0]?.statistics_unit_of_measurement; return html` - metadata.every((metaData) => + metadata!.every((metaData) => statisticsMetaHasType(metaData, statTypeMap[stat_type]) ) ); @@ -259,18 +297,27 @@ export class HuiStatisticsGraphCardEditor delete config.stat_types; } } + if ( + config.unit && + !metadata!.some( + (metaData) => + getDisplayUnit(this.hass!, metaData?.statistic_id, metaData) === + config.unit + ) + ) { + delete config.unit; + } fireEvent(this, "config-changed", { config, }); } - private _computeLabelCallback = ( - schema: SchemaUnion> - ) => { + private _computeLabelCallback = (schema) => { switch (schema.name) { case "chart_type": case "stat_types": case "period": + case "unit": return this.hass!.localize( `ui.panel.lovelace.editor.card.statistics-graph.${schema.name}` ); diff --git a/src/translations/en.json b/src/translations/en.json index 23a491fe9f..562e1ef088 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4052,6 +4052,7 @@ "name": "Statistics Graph", "description": "The Statistics Graph card allows you to display a graph of the statistics for each of the entities listed.", "period": "Period", + "unit": "Unit", "stat_types": "Show stat types", "stat_type_labels": { "mean": "Mean",