mirror of
https://github.com/home-assistant/frontend.git
synced 2025-10-18 08:09:44 +00:00
Compare commits
9 Commits
power
...
power-char
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8b3b7e701a | ||
![]() |
299c08c0ea | ||
![]() |
2fd33069c1 | ||
![]() |
8dde4c9a21 | ||
![]() |
7a2826a580 | ||
![]() |
c32e5a049a | ||
![]() |
f294c1bae6 | ||
![]() |
4aadd7ec71 | ||
![]() |
f68d885dd8 |
@@ -360,6 +360,35 @@ export const getReferencedStatisticIds = (
|
||||
return statIDs;
|
||||
};
|
||||
|
||||
export const getReferencedStatisticIdsPower = (
|
||||
prefs: EnergyPreferences
|
||||
): string[] => {
|
||||
const statIDs: (string | undefined)[] = [];
|
||||
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "gas" || source.type === "water") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.type === "solar") {
|
||||
statIDs.push(source.stat_power);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.type === "battery") {
|
||||
statIDs.push(source.stat_power);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.power) {
|
||||
statIDs.push(...source.power.map((p) => p.stat_power));
|
||||
}
|
||||
}
|
||||
statIDs.push(...prefs.device_consumption.map((d) => d.stat_power));
|
||||
|
||||
return statIDs.filter(Boolean) as string[];
|
||||
};
|
||||
|
||||
export const enum CompareMode {
|
||||
NONE = "",
|
||||
PREVIOUS = "previous",
|
||||
@@ -407,9 +436,10 @@ const getEnergyData = async (
|
||||
"gas",
|
||||
"device",
|
||||
]);
|
||||
const powerStatIds = getReferencedStatisticIdsPower(prefs);
|
||||
const waterStatIds = getReferencedStatisticIds(prefs, info, ["water"]);
|
||||
|
||||
const allStatIDs = [...energyStatIds, ...waterStatIds];
|
||||
const allStatIDs = [...energyStatIds, ...waterStatIds, ...powerStatIds];
|
||||
|
||||
const dayDifference = differenceInDays(end || new Date(), start);
|
||||
const period =
|
||||
@@ -420,6 +450,8 @@ const getEnergyData = async (
|
||||
: dayDifference > 2
|
||||
? "day"
|
||||
: "hour";
|
||||
const finePeriod =
|
||||
dayDifference > 64 ? "day" : dayDifference > 8 ? "hour" : "5minute";
|
||||
|
||||
const statsMetadata: Record<string, StatisticsMetaData> = {};
|
||||
const statsMetadataArray = allStatIDs.length
|
||||
@@ -441,6 +473,9 @@ const getEnergyData = async (
|
||||
? (gasUnit as (typeof VOLUME_UNITS)[number])
|
||||
: undefined,
|
||||
};
|
||||
const powerUnits: StatisticsUnitConfiguration = {
|
||||
power: "kW",
|
||||
};
|
||||
const waterUnit = getEnergyWaterUnit(hass, prefs, statsMetadata);
|
||||
const waterUnits: StatisticsUnitConfiguration = {
|
||||
volume: waterUnit,
|
||||
@@ -451,6 +486,12 @@ const getEnergyData = async (
|
||||
"change",
|
||||
])
|
||||
: {};
|
||||
const _powerStats: Statistics | Promise<Statistics> = powerStatIds.length
|
||||
? fetchStatistics(hass!, start, end, powerStatIds, finePeriod, powerUnits, [
|
||||
"mean",
|
||||
])
|
||||
: {};
|
||||
|
||||
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
|
||||
? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [
|
||||
"change",
|
||||
@@ -557,6 +598,7 @@ const getEnergyData = async (
|
||||
|
||||
const [
|
||||
energyStats,
|
||||
powerStats,
|
||||
waterStats,
|
||||
energyStatsCompare,
|
||||
waterStatsCompare,
|
||||
@@ -564,13 +606,14 @@ const getEnergyData = async (
|
||||
fossilEnergyConsumptionCompare,
|
||||
] = await Promise.all([
|
||||
_energyStats,
|
||||
_powerStats,
|
||||
_waterStats,
|
||||
_energyStatsCompare,
|
||||
_waterStatsCompare,
|
||||
_fossilEnergyConsumption,
|
||||
_fossilEnergyConsumptionCompare,
|
||||
]);
|
||||
const stats = { ...energyStats, ...waterStats };
|
||||
const stats = { ...energyStats, ...waterStats, ...powerStats };
|
||||
if (compare) {
|
||||
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ import {
|
||||
import type {
|
||||
BarSeriesOption,
|
||||
CallbackDataParams,
|
||||
LineSeriesOption,
|
||||
TopLevelFormatterParams,
|
||||
} from "echarts/types/dist/shared";
|
||||
import type { FrontendLocaleData } from "../../../../../data/translation";
|
||||
@@ -170,11 +171,10 @@ function formatTooltip(
|
||||
compare
|
||||
? `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}: `
|
||||
: ""
|
||||
}${formatTime(date, locale, config)} – ${formatTime(
|
||||
addHours(date, 1),
|
||||
locale,
|
||||
config
|
||||
)}`;
|
||||
}${formatTime(date, locale, config)}`;
|
||||
if (params[0].componentSubType === "bar") {
|
||||
period += ` – ${formatTime(addHours(date, 1), locale, config)}`;
|
||||
}
|
||||
}
|
||||
const title = `<h4 style="text-align: center; margin: 0;">${period}</h4>`;
|
||||
|
||||
@@ -281,6 +281,35 @@ export function fillDataGapsAndRoundCaps(datasets: BarSeriesOption[]) {
|
||||
});
|
||||
}
|
||||
|
||||
export function fillLineGaps(datasets: LineSeriesOption[]) {
|
||||
const buckets = Array.from(
|
||||
new Set(
|
||||
datasets
|
||||
.map((dataset) =>
|
||||
dataset.data!.map((datapoint) => Number(datapoint![0]))
|
||||
)
|
||||
.flat()
|
||||
)
|
||||
).sort((a, b) => a - b);
|
||||
buckets.forEach((bucket, index) => {
|
||||
for (let i = datasets.length - 1; i >= 0; i--) {
|
||||
const dataPoint = datasets[i].data![index];
|
||||
const item: any =
|
||||
dataPoint && typeof dataPoint === "object" && "value" in dataPoint
|
||||
? dataPoint
|
||||
: { value: dataPoint };
|
||||
const x = item.value?.[0];
|
||||
if (x === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (Number(x) !== bucket) {
|
||||
datasets[i].data?.splice(index, 0, [bucket, 0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
return datasets;
|
||||
}
|
||||
|
||||
export function getCompareTransform(start: Date, compareStart?: Date) {
|
||||
if (!compareStart) {
|
||||
return (ts: Date) => ts;
|
||||
|
305
src/panels/lovelace/cards/energy/hui-power-sources-graph-card.ts
Normal file
305
src/panels/lovelace/cards/energy/hui-power-sources-graph-card.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
import { endOfToday, isToday, startOfToday } from "date-fns";
|
||||
import type { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
import { graphic } from "echarts";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import { getEnergyDataCollection } from "../../../../data/energy";
|
||||
import type { StatisticValue } from "../../../../data/recorder";
|
||||
import type { FrontendLocaleData } from "../../../../data/translation";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceCard } from "../../types";
|
||||
import type { PowerSourcesGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import { getCommonOptions, fillLineGaps } from "./common/energy-chart-options";
|
||||
import type { ECOption } from "../../../../resources/echarts";
|
||||
import { hex2rgb } from "../../../../common/color/convert-color";
|
||||
|
||||
@customElement("hui-power-sources-graph-card")
|
||||
export class HuiPowerSourcesGraphCard
|
||||
extends SubscribeMixin(LitElement)
|
||||
implements LovelaceCard
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _config?: PowerSourcesGraphCardConfig;
|
||||
|
||||
@state() private _chartData: LineSeriesOption[] = [];
|
||||
|
||||
@state() private _start = startOfToday();
|
||||
|
||||
@state() private _end = endOfToday();
|
||||
|
||||
@state() private _compareStart?: Date;
|
||||
|
||||
@state() private _compareEnd?: Date;
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
getEnergyDataCollection(this.hass, {
|
||||
key: this._config?.collection_key,
|
||||
}).subscribe((data) => this._getStatistics(data)),
|
||||
];
|
||||
}
|
||||
|
||||
public getCardSize(): Promise<number> | number {
|
||||
return 3;
|
||||
}
|
||||
|
||||
public setConfig(config: PowerSourcesGraphCardConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return (
|
||||
hasConfigChanged(this, changedProps) ||
|
||||
changedProps.size > 1 ||
|
||||
!changedProps.has("hass")
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
${this._config.title
|
||||
? html`<h1 class="card-header">${this._config.title}</h1>`
|
||||
: ""}
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"has-header": !!this._config.title,
|
||||
})}"
|
||||
>
|
||||
<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.data=${this._chartData}
|
||||
.options=${this._createOptions(
|
||||
this._start,
|
||||
this._end,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this._compareStart,
|
||||
this._compareEnd
|
||||
)}
|
||||
></ha-chart-base>
|
||||
${!this._chartData.some((dataset) => dataset.data!.length)
|
||||
? html`<div class="no-data">
|
||||
${isToday(this._start)
|
||||
? this.hass.localize("ui.panel.lovelace.cards.energy.no_data")
|
||||
: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.no_data_period"
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _createOptions = memoizeOne(
|
||||
(
|
||||
start: Date,
|
||||
end: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
compareStart?: Date,
|
||||
compareEnd?: Date
|
||||
): ECOption =>
|
||||
getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
locale,
|
||||
config,
|
||||
"kW",
|
||||
compareStart,
|
||||
compareEnd
|
||||
)
|
||||
);
|
||||
|
||||
private async _getStatistics(energyData: EnergyData): Promise<void> {
|
||||
const datasets: LineSeriesOption[] = [];
|
||||
|
||||
const statIds = {
|
||||
solar: {
|
||||
stats: [] as string[],
|
||||
color: "--energy-solar-color",
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.power_graph.solar"
|
||||
),
|
||||
},
|
||||
grid: {
|
||||
stats: [] as string[],
|
||||
color: "--energy-grid-consumption-color",
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.power_graph.grid"
|
||||
),
|
||||
},
|
||||
battery: {
|
||||
stats: [] as string[],
|
||||
color: "--energy-battery-out-color",
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.power_graph.battery"
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
const computedStyles = getComputedStyle(this);
|
||||
|
||||
for (const source of energyData.prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
if (source.stat_power) {
|
||||
statIds.solar.stats.push(source.stat_power);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.type === "battery") {
|
||||
if (source.stat_power) {
|
||||
statIds.battery.stats.push(source.stat_power);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.type === "grid" && source.power) {
|
||||
statIds.grid.stats.push(...source.power.map((p) => p.stat_power));
|
||||
}
|
||||
}
|
||||
const commonSeriesOptions: LineSeriesOption = {
|
||||
type: "line",
|
||||
smooth: 0.4,
|
||||
smoothMonotone: "x",
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
};
|
||||
|
||||
Object.keys(statIds).forEach((key, keyIndex) => {
|
||||
if (statIds[key].stats.length) {
|
||||
const colorHex = computedStyles.getPropertyValue(statIds[key].color);
|
||||
const rgb = hex2rgb(colorHex);
|
||||
const { positive, negative } = this._processData(
|
||||
statIds[key].stats.map((id: string) => energyData.stats[id] ?? [])
|
||||
);
|
||||
datasets.push({
|
||||
...commonSeriesOptions,
|
||||
id: key,
|
||||
name: statIds[key].name,
|
||||
color: colorHex,
|
||||
stack: "positive",
|
||||
areaStyle: {
|
||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`,
|
||||
},
|
||||
]),
|
||||
},
|
||||
data: positive,
|
||||
z: 3 - keyIndex, // draw in reverse order so 0 value lines are overwritten
|
||||
});
|
||||
if (key !== "solar") {
|
||||
datasets.push({
|
||||
...commonSeriesOptions,
|
||||
id: `${key}-negative`,
|
||||
name: statIds[key].name,
|
||||
color: colorHex,
|
||||
stack: "negative",
|
||||
areaStyle: {
|
||||
color: new graphic.LinearGradient(0, 1, 0, 0, [
|
||||
{
|
||||
offset: 0,
|
||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`,
|
||||
},
|
||||
]),
|
||||
},
|
||||
data: negative,
|
||||
z: 4 - keyIndex, // draw in reverse order but above positive series
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._start = energyData.start;
|
||||
this._end = energyData.end || endOfToday();
|
||||
|
||||
this._chartData = fillLineGaps(datasets);
|
||||
}
|
||||
|
||||
private _processData(stats: StatisticValue[][]) {
|
||||
const data: Record<number, number[]> = {};
|
||||
stats.forEach((statSet) => {
|
||||
statSet.forEach((point) => {
|
||||
if (point.mean == null) {
|
||||
return;
|
||||
}
|
||||
const x = (point.start + point.end) / 2;
|
||||
data[x] = [...(data[x] ?? []), point.mean];
|
||||
});
|
||||
});
|
||||
const positive: [number, number][] = [];
|
||||
const negative: [number, number][] = [];
|
||||
Object.entries(data).forEach(([x, y]) => {
|
||||
const ts = Number(x);
|
||||
const meanY = y.reduce((a, b) => a + b, 0) / y.length;
|
||||
positive.push([ts, Math.max(0, meanY)]);
|
||||
negative.push([ts, Math.min(0, meanY)]);
|
||||
});
|
||||
return { positive, negative };
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
}
|
||||
.card-header {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.content {
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
.has-header {
|
||||
padding-top: 0;
|
||||
}
|
||||
.no-data {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20%;
|
||||
margin-left: var(--ha-space-8);
|
||||
margin-inline-start: var(--ha-space-8);
|
||||
margin-inline-end: initial;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-power-sources-graph-card": HuiPowerSourcesGraphCard;
|
||||
}
|
||||
}
|
@@ -227,6 +227,11 @@ export interface EnergySankeyCardConfig extends EnergyCardBaseConfig {
|
||||
group_by_area?: boolean;
|
||||
}
|
||||
|
||||
export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig {
|
||||
type: "power-sources-graph";
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface EntityFilterCardConfig extends LovelaceCardConfig {
|
||||
type: "entity-filter";
|
||||
entities: (EntityFilterEntityConfig | string)[];
|
||||
|
@@ -66,6 +66,8 @@ const LAZY_LOAD_TYPES = {
|
||||
"energy-usage-graph": () =>
|
||||
import("../cards/energy/hui-energy-usage-graph-card"),
|
||||
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
|
||||
"power-sources-graph": () =>
|
||||
import("../cards/energy/hui-power-sources-graph-card"),
|
||||
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
||||
error: () => import("../cards/hui-error-card"),
|
||||
"home-summary": () => import("../cards/hui-home-summary-card"),
|
||||
|
@@ -22,6 +22,7 @@ const NON_STANDARD_URLS = {
|
||||
"energy-devices-graph": "energy/#devices-energy-graph",
|
||||
"energy-devices-detail-graph": "energy/#detail-devices-energy-graph",
|
||||
"energy-sankey": "energy/#sankey-energy-graph",
|
||||
"power-sources-graph": "energy/#power-sources-graph",
|
||||
};
|
||||
|
||||
export const getCardDocumentationURL = (
|
||||
|
@@ -7053,6 +7053,11 @@
|
||||
"card_indicates_energy_used": "This card indicates how much of the electricity consumed by your home was generated using non-fossil fuels like solar, wind, and nuclear. The higher, the better!",
|
||||
"low_carbon_energy_consumed": "Low-carbon electricity consumed",
|
||||
"low_carbon_energy_not_calculated": "Consumed low-carbon electricity couldn't be calculated"
|
||||
},
|
||||
"power_graph": {
|
||||
"grid": "Grid",
|
||||
"solar": "Solar",
|
||||
"battery": "Battery"
|
||||
}
|
||||
},
|
||||
"heading": {
|
||||
|
Reference in New Issue
Block a user