mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Add statistic card (#14198)
* Add statistic card * Update selector.ts * Update translations * review
This commit is contained in:
parent
6326bb010f
commit
9d730919d5
53
src/components/ha-selector/ha-selector-statistic.ts
Normal file
53
src/components/ha-selector/ha-selector-statistic.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { StatisticSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-statistics-picker";
|
||||
|
||||
@customElement("ha-selector-statistic")
|
||||
export class HaStatisticSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: StatisticSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
if (!this.selector.statistic.multiple) {
|
||||
return html`<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
allow-custom-entity
|
||||
></ha-statistic-picker>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : ""}
|
||||
<ha-statistics-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-statistics-picker>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-statistic": HaStatisticSelector;
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ const LOAD_ELEMENTS = {
|
||||
device: () => import("./ha-selector-device"),
|
||||
duration: () => import("./ha-selector-duration"),
|
||||
entity: () => import("./ha-selector-entity"),
|
||||
statistic: () => import("./ha-selector-statistic"),
|
||||
file: () => import("./ha-selector-file"),
|
||||
navigation: () => import("./ha-selector-navigation"),
|
||||
number: () => import("./ha-selector-number"),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { HaDurationData } from "../components/ha-duration-input";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export type StatisticType = "state" | "sum" | "min" | "max" | "mean";
|
||||
@ -19,6 +20,13 @@ export interface StatisticValue {
|
||||
state: number | null;
|
||||
}
|
||||
|
||||
export interface Statistic {
|
||||
max: number | null;
|
||||
mean: number | null;
|
||||
min: number | null;
|
||||
change: number | null;
|
||||
}
|
||||
|
||||
export interface StatisticsMetaData {
|
||||
statistics_unit_of_measurement: string | null;
|
||||
statistic_id: string;
|
||||
@ -122,6 +130,36 @@ export const fetchStatistics = (
|
||||
units,
|
||||
});
|
||||
|
||||
export const fetchStatistic = (
|
||||
hass: HomeAssistant,
|
||||
statistic_id: string,
|
||||
period: {
|
||||
fixed_period?: { start: string | Date; end: string | Date };
|
||||
calendar?: { period: string; offset: number };
|
||||
rolling_window?: { duration: HaDurationData; offset: HaDurationData };
|
||||
},
|
||||
units?: StatisticsUnitConfiguration
|
||||
) =>
|
||||
hass.callWS<Statistic>({
|
||||
type: "recorder/statistic_during_period",
|
||||
statistic_id,
|
||||
units,
|
||||
fixed_period: period.fixed_period
|
||||
? {
|
||||
start_time:
|
||||
period.fixed_period.start instanceof Date
|
||||
? period.fixed_period.start.toISOString()
|
||||
: period.fixed_period.start,
|
||||
end_time:
|
||||
period.fixed_period.end instanceof Date
|
||||
? period.fixed_period.end.toISOString()
|
||||
: period.fixed_period.end,
|
||||
}
|
||||
: undefined,
|
||||
calendar: period.calendar,
|
||||
rolling_window: period.rolling_window,
|
||||
});
|
||||
|
||||
export const validateStatistics = (hass: HomeAssistant) =>
|
||||
hass.callWS<StatisticsValidationResults>({
|
||||
type: "recorder/validate_statistics",
|
||||
|
@ -27,6 +27,7 @@ export type Selector =
|
||||
| ObjectSelector
|
||||
| SelectSelector
|
||||
| StateSelector
|
||||
| StatisticSelector
|
||||
| StringSelector
|
||||
| TargetSelector
|
||||
| TemplateSelector
|
||||
@ -133,6 +134,13 @@ export interface EntitySelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface StatisticSelector {
|
||||
statistic: {
|
||||
device_class?: string;
|
||||
multiple?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FileSelector {
|
||||
file: {
|
||||
accept: string;
|
||||
@ -195,7 +203,7 @@ export interface ObjectSelector {
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
value: string;
|
||||
value: any;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
316
src/panels/lovelace/cards/hui-statistic-card.ts
Normal file
316
src/panels/lovelace/cards/hui-statistic-card.ts
Normal file
@ -0,0 +1,316 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-state-icon";
|
||||
import {
|
||||
fetchStatistic,
|
||||
getDisplayUnit,
|
||||
getStatisticLabel,
|
||||
getStatisticMetadata,
|
||||
isExternalStatistic,
|
||||
StatisticsMetaData,
|
||||
} from "../../../data/recorder";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { createHeaderFooterElement } from "../create-element/create-header-footer-element";
|
||||
import {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceHeaderFooter,
|
||||
} from "../types";
|
||||
import { HuiErrorCard } from "./hui-error-card";
|
||||
import { EntityCardConfig, StatisticCardConfig } from "./types";
|
||||
|
||||
@customElement("hui-statistic-card")
|
||||
export class HuiStatisticCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
await import("../editor/config-elements/hui-statistic-card-editor");
|
||||
return document.createElement("hui-statistic-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
entities: string[],
|
||||
entitiesFill: string[]
|
||||
) {
|
||||
const includeDomains = ["sensor"];
|
||||
const maxEntities = 1;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
includeDomains,
|
||||
(stateObj: HassEntity) => "state_class" in stateObj.attributes
|
||||
);
|
||||
|
||||
return {
|
||||
entity: foundEntities[0] || "",
|
||||
period: { calendar: { period: "month", offset: 0 } },
|
||||
};
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: StatisticCardConfig;
|
||||
|
||||
@state() private _value?: number | null;
|
||||
|
||||
@state() private _metadata?: StatisticsMetaData;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _interval?: number;
|
||||
|
||||
private _footerElement?: HuiErrorCard | LovelaceHeaderFooter;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
clearInterval(this._interval);
|
||||
}
|
||||
|
||||
public setConfig(config: StatisticCardConfig): void {
|
||||
if (!config.entity) {
|
||||
throw new Error("Entity must be specified");
|
||||
}
|
||||
if (!config.stat_type) {
|
||||
throw new Error("Statistic type must be specified");
|
||||
}
|
||||
if (!config.period) {
|
||||
throw new Error("Period must be specified");
|
||||
}
|
||||
if (
|
||||
config.entity &&
|
||||
!isExternalStatistic(config.entity) &&
|
||||
!isValidEntityId(config.entity)
|
||||
) {
|
||||
throw new Error("Invalid entity");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
this._error = undefined;
|
||||
this._fetchStatistic();
|
||||
this._fetchMetadata();
|
||||
|
||||
if (this._config.footer) {
|
||||
this._footerElement = createHeaderFooterElement(this._config.footer);
|
||||
} else if (this._footerElement) {
|
||||
this._footerElement = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async getCardSize(): Promise<number> {
|
||||
let size = 2;
|
||||
if (this._footerElement) {
|
||||
const footerSize = computeCardSize(this._footerElement);
|
||||
size += footerSize instanceof Promise ? await footerSize : footerSize;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config || !this.hass) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (this._error) {
|
||||
return html` <ha-alert alert-type="error">${this._error}</ha-alert> `;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
const name =
|
||||
this._config.name ||
|
||||
getStatisticLabel(this.hass, this._config.entity, this._metadata);
|
||||
|
||||
return html`
|
||||
<ha-card @click=${this._handleClick} tabindex="0">
|
||||
<div class="header">
|
||||
<div class="name" .title=${name}>${name}</div>
|
||||
<div class="icon">
|
||||
<ha-state-icon
|
||||
.icon=${this._config.icon}
|
||||
.state=${stateObj}
|
||||
></ha-state-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="value"
|
||||
>${this._value === undefined
|
||||
? ""
|
||||
: this._value === null
|
||||
? "?"
|
||||
: formatNumber(this._value, this.hass.locale)}</span
|
||||
>
|
||||
<span class="measurement"
|
||||
>${this._config.unit ||
|
||||
getDisplayUnit(
|
||||
this.hass,
|
||||
this._config.entity,
|
||||
this._metadata
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
${this._footerElement}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
// Side Effect used to update footer hass while keeping optimizations
|
||||
if (this._footerElement) {
|
||||
this._footerElement.hass = this.hass;
|
||||
}
|
||||
if (
|
||||
changedProps.has("_value") ||
|
||||
changedProps.has("_metadata") ||
|
||||
changedProps.has("_error")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (this._config) {
|
||||
return hasConfigOrEntityChanged(this, changedProps);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._fetchStatistic();
|
||||
this._fetchMetadata();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const oldConfig = changedProps.get("_config") as
|
||||
| EntityCardConfig
|
||||
| undefined;
|
||||
|
||||
if (
|
||||
!oldHass ||
|
||||
!oldConfig ||
|
||||
oldHass.themes !== this.hass.themes ||
|
||||
oldConfig.theme !== this._config.theme
|
||||
) {
|
||||
applyThemesOnElement(this, this.hass.themes, this._config!.theme);
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchStatistic() {
|
||||
if (!this.hass || !this._config) {
|
||||
return;
|
||||
}
|
||||
clearInterval(this._interval);
|
||||
this._interval = window.setInterval(
|
||||
() => this._fetchStatistic(),
|
||||
5 * 1000 * 60
|
||||
);
|
||||
try {
|
||||
const stats = await fetchStatistic(
|
||||
this.hass,
|
||||
this._config.entity,
|
||||
this._config.period
|
||||
);
|
||||
this._value = stats[this._config!.stat_type];
|
||||
this._error = undefined;
|
||||
} catch (e: any) {
|
||||
this._error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchMetadata() {
|
||||
if (!this.hass || !this._config) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._metadata = (
|
||||
await getStatisticMetadata(this.hass, [this._config.entity])
|
||||
)?.[0];
|
||||
} catch (e: any) {
|
||||
this._error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleClick(): void {
|
||||
fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 8px 16px 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: var(--secondary-text-color);
|
||||
line-height: 40px;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--state-icon-color, #44739e);
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 0px 16px 16px;
|
||||
margin-top: -4px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.measurement {
|
||||
font-size: 18px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-statistic-card": HuiStatisticCard;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { StatisticType } from "../../../data/recorder";
|
||||
import { Statistic, StatisticType } from "../../../data/recorder";
|
||||
import { ActionConfig, LovelaceCardConfig } from "../../../data/lovelace";
|
||||
import { FullCalendarView, TranslationDict } from "../../../types";
|
||||
import { Condition } from "../common/validate-condition";
|
||||
@ -10,6 +10,7 @@ import {
|
||||
LovelaceRowConfig,
|
||||
} from "../entity-rows/types";
|
||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import { HaDurationData } from "../../../components/ha-duration-input";
|
||||
|
||||
export interface AlarmPanelCardConfig extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
@ -309,6 +310,18 @@ export interface StatisticsGraphCardConfig extends LovelaceCardConfig {
|
||||
chart_type?: "line" | "bar";
|
||||
}
|
||||
|
||||
export interface StatisticCardConfig extends LovelaceCardConfig {
|
||||
title?: string;
|
||||
entities: Array<EntityConfig | string>;
|
||||
period: {
|
||||
fixed_period?: { start: string; end: string };
|
||||
calendar?: { period: string; offset: number };
|
||||
rolling_window?: { duration: HaDurationData; offset: HaDurationData };
|
||||
};
|
||||
stat_type: keyof Statistic;
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
export interface PictureCardConfig extends LovelaceCardConfig {
|
||||
image?: string;
|
||||
tap_action?: ActionConfig;
|
||||
|
@ -79,6 +79,7 @@ const LAZY_LOAD_TYPES = {
|
||||
"shopping-list": () => import("../cards/hui-shopping-list-card"),
|
||||
starting: () => import("../cards/hui-starting-card"),
|
||||
"statistics-graph": () => import("../cards/hui-statistics-graph-card"),
|
||||
statistic: () => import("../cards/hui-statistic-card"),
|
||||
"vertical-stack": () => import("../cards/hui-vertical-stack-card"),
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,266 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket/dist/types";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { any, assert, assign, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { domainIcon } from "../../../../common/entity/domain_icon";
|
||||
import { 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 {
|
||||
getStatisticMetadata,
|
||||
StatisticsMetaData,
|
||||
statisticsMetaHasType,
|
||||
StatisticType,
|
||||
} from "../../../../data/recorder";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { StatisticCardConfig } from "../../cards/types";
|
||||
import { headerFooterConfigStructs } from "../../header-footer/structs";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
entity: optional(string()),
|
||||
name: optional(string()),
|
||||
icon: optional(string()),
|
||||
unit: optional(string()),
|
||||
stat_type: optional(string()),
|
||||
period: optional(any()),
|
||||
theme: optional(string()),
|
||||
footer: optional(headerFooterConfigStructs),
|
||||
})
|
||||
);
|
||||
|
||||
const stat_types = ["mean", "min", "max", "change"] as const;
|
||||
|
||||
const statTypeMap: Record<typeof stat_types[number], StatisticType> = {
|
||||
mean: "mean",
|
||||
min: "min",
|
||||
max: "max",
|
||||
change: "sum",
|
||||
};
|
||||
|
||||
const periods = {
|
||||
today: { calendar: { period: "day" } },
|
||||
yesterday: { calendar: { period: "day", offset: -1 } },
|
||||
this_week: { calendar: { period: "week" } },
|
||||
last_week: { calendar: { period: "week", offset: -1 } },
|
||||
this_month: { calendar: { period: "month" } },
|
||||
last_month: { calendar: { period: "month", offset: -1 } },
|
||||
this_year: { calendar: { period: "year" } },
|
||||
last_year: { calendar: { period: "year", offset: -1 } },
|
||||
} as const;
|
||||
|
||||
@customElement("hui-statistic-card-editor")
|
||||
export class HuiStatisticCardEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: StatisticCardConfig;
|
||||
|
||||
@state() private _metadata?: StatisticsMetaData;
|
||||
|
||||
public setConfig(config: StatisticCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
this._fetchMetadata();
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this._fetchMetadata().then(() => {
|
||||
if (!this._config?.stat_type && this._config?.entity) {
|
||||
fireEvent(this, "config-changed", {
|
||||
config: {
|
||||
...this._config,
|
||||
stat_type: this._metadata?.has_sum ? "change" : "mean",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _data = memoizeOne((config: StatisticCardConfig) => {
|
||||
if (!config || !config.period) {
|
||||
return config;
|
||||
}
|
||||
for (const period of Object.values(periods)) {
|
||||
if (deepEqual(period, config.period)) {
|
||||
return { ...config, period };
|
||||
}
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
entity: string,
|
||||
icon: string,
|
||||
periodVal: any,
|
||||
entityState: HassEntity,
|
||||
localize: LocalizeFunc,
|
||||
metadata?: StatisticsMetaData
|
||||
) =>
|
||||
[
|
||||
{ name: "entity", required: true, selector: { statistic: {} } },
|
||||
{
|
||||
name: "stat_type",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
multiple: false,
|
||||
options: stat_types.map((stat_type) => ({
|
||||
value: stat_type,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.statistic.stat_type_labels.${stat_type}`
|
||||
),
|
||||
disabled:
|
||||
!metadata ||
|
||||
!statisticsMetaHasType(metadata, statTypeMap[stat_type]),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "period",
|
||||
required: true,
|
||||
selector: Object.values(periods).includes(periodVal)
|
||||
? {
|
||||
select: {
|
||||
multiple: false,
|
||||
options: Object.entries(periods).map(
|
||||
([periodKey, period]) => ({
|
||||
value: period,
|
||||
label:
|
||||
localize(
|
||||
`ui.panel.lovelace.editor.card.statistic.periods.${periodKey}`
|
||||
) || periodKey,
|
||||
})
|
||||
),
|
||||
},
|
||||
}
|
||||
: { object: {} },
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
name: "",
|
||||
schema: [
|
||||
{ name: "name", selector: { text: {} } },
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {
|
||||
placeholder: icon || entityState?.attributes.icon,
|
||||
fallbackPath:
|
||||
!icon && !entityState?.attributes.icon && entityState
|
||||
? domainIcon(computeDomain(entity), entityState)
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: "unit", selector: { text: {} } },
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
],
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const entityState = this.hass.states[this._config.entity];
|
||||
|
||||
const data = this._data(this._config);
|
||||
|
||||
const schema = this._schema(
|
||||
this._config.entity,
|
||||
this._config.icon,
|
||||
data.period,
|
||||
entityState,
|
||||
this.hass.localize,
|
||||
this._metadata
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchMetadata() {
|
||||
if (!this.hass || !this._config) {
|
||||
return;
|
||||
}
|
||||
this._metadata = (
|
||||
await getStatisticMetadata(this.hass, [this._config.entity])
|
||||
)[0];
|
||||
}
|
||||
|
||||
private async _valueChanged(ev: CustomEvent) {
|
||||
const config = ev.detail.value as StatisticCardConfig;
|
||||
Object.keys(config).forEach((k) => config[k] === "" && delete config[k]);
|
||||
if (
|
||||
config.stat_type &&
|
||||
config.entity &&
|
||||
config.entity !== this._metadata?.statistic_id
|
||||
) {
|
||||
const metadata = (
|
||||
await getStatisticMetadata(this.hass!, [config.entity])
|
||||
)?.[0];
|
||||
if (metadata && !metadata.has_sum && config.stat_type === "change") {
|
||||
config.stat_type = "mean";
|
||||
}
|
||||
if (metadata && !metadata.has_mean && config.stat_type !== "change") {
|
||||
config.stat_type = "change";
|
||||
}
|
||||
}
|
||||
if (!config.stat_type && config.entity) {
|
||||
const metadata = (
|
||||
await getStatisticMetadata(this.hass!, [config.entity])
|
||||
)?.[0];
|
||||
config.stat_type = metadata?.has_sum ? "change" : "mean";
|
||||
}
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
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"
|
||||
)})`;
|
||||
}
|
||||
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-statistic-card-editor": HuiStatisticCardEditor;
|
||||
}
|
||||
}
|
@ -37,6 +37,10 @@ export const coreCards: Card[] = [
|
||||
type: "statistics-graph",
|
||||
showElement: false,
|
||||
},
|
||||
{
|
||||
type: "statistic",
|
||||
showElement: true,
|
||||
},
|
||||
{
|
||||
type: "humidifier",
|
||||
showElement: true,
|
||||
|
@ -4065,6 +4065,28 @@
|
||||
"pick_statistic": "Add a statistic",
|
||||
"picked_statistic": "Statistic"
|
||||
},
|
||||
"statistic": {
|
||||
"name": "Statistic",
|
||||
"description": "The Statistic card allows you to display a statistical value of an entity of a certain period.",
|
||||
"period": "Period",
|
||||
"stat_types": "Show stat",
|
||||
"stat_type_labels": {
|
||||
"mean": "Mean",
|
||||
"min": "Min",
|
||||
"max": "Max",
|
||||
"change": "Change"
|
||||
},
|
||||
"periods": {
|
||||
"today": "Today",
|
||||
"yesterday": "Yesterday",
|
||||
"this_week": "This week",
|
||||
"last_week": "Last week",
|
||||
"this_month": "This month",
|
||||
"last_month": "Last month",
|
||||
"this_year": "This year",
|
||||
"last_year": "Last year"
|
||||
}
|
||||
},
|
||||
"horizontal-stack": {
|
||||
"name": "Horizontal Stack",
|
||||
"description": "The Horizontal Stack card allows you to stack together multiple cards, so they always sit next to each other in the space of one column."
|
||||
|
Loading…
x
Reference in New Issue
Block a user