Energy dashboard tweaks and fixes (#9643)

* Energy dashboard tweaks and fixes

* Make headers smaller

* Change button styling in onboarding

* Disable add when no stat choosen

* Oops

* Update hui-energy-carbon-consumed-gauge-card.ts

* Update hui-energy-distribution-card.ts
This commit is contained in:
Bram Kragten 2021-07-29 19:44:33 +02:00 committed by GitHub
parent 6e7af18494
commit 1531e99528
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 780 additions and 529 deletions

View File

@ -1,5 +1,22 @@
import { FrontendLocaleData, NumberFormat } from "../../data/translation";
export const numberFormatToLocale = (
localeOptions: FrontendLocaleData
): string | string[] | undefined => {
switch (localeOptions.number_format) {
case NumberFormat.comma_decimal:
return ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89
case NumberFormat.decimal_comma:
return ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
case NumberFormat.space_comma:
return ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
case NumberFormat.system:
return undefined;
default:
return localeOptions.language;
}
};
/**
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
*
@ -9,27 +26,12 @@ import { FrontendLocaleData, NumberFormat } from "../../data/translation";
*/
export const formatNumber = (
num: string | number,
locale?: FrontendLocaleData,
localeOptions?: FrontendLocaleData,
options?: Intl.NumberFormatOptions
): string => {
let format: string | string[] | undefined;
switch (locale?.number_format) {
case NumberFormat.comma_decimal:
format = ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89
break;
case NumberFormat.decimal_comma:
format = ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
break;
case NumberFormat.space_comma:
format = ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
break;
case NumberFormat.system:
format = undefined;
break;
default:
format = locale?.language;
}
const locale = localeOptions
? numberFormatToLocale(localeOptions)
: undefined;
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
Number.isNaN =
@ -39,13 +41,13 @@ export const formatNumber = (
};
if (
localeOptions?.number_format !== NumberFormat.none &&
!Number.isNaN(Number(num)) &&
Intl &&
locale?.number_format !== NumberFormat.none
Intl
) {
try {
return new Intl.NumberFormat(
format,
locale,
getDefaultFormatOptions(num, options)
).format(Number(num));
} catch (error) {

View File

@ -152,7 +152,17 @@ export default class HaChartBase extends LitElement {
.querySelector("canvas")!
.getContext("2d")!;
this.chart = new (await import("../../resources/chartjs")).Chart(ctx, {
const ChartConstructor = (await import("../../resources/chartjs")).Chart;
const computedStyles = getComputedStyle(this);
ChartConstructor.defaults.borderColor =
computedStyles.getPropertyValue("--divider-color");
ChartConstructor.defaults.color = computedStyles.getPropertyValue(
"--secondary-text-color"
);
this.chart = new ChartConstructor(ctx, {
type: this.chartType,
data: this.data,
options: this._createOptions(),

View File

@ -109,6 +109,8 @@ class StateHistoryChartLine extends LitElement {
hitRadius: 5,
},
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
};
}
if (changedProps.has("data")) {

View File

@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { computeDomain } from "../../common/entity/compute_domain";
import { numberFormatToLocale } from "../../common/string/format_number";
import { computeRTL } from "../../common/util/compute_rtl";
import { TimelineEntity } from "../../data/history";
import { HomeAssistant } from "../../types";
@ -186,6 +187,8 @@ export class StateHistoryChartTimeline extends LitElement {
propagate: true,
},
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
};
}
if (changedProps.has("data")) {

View File

@ -16,6 +16,7 @@ import { customElement, property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { computeStateName } from "../../common/entity/compute_state_name";
import { numberFormatToLocale } from "../../common/string/format_number";
import {
Statistics,
statisticsHaveType,
@ -119,7 +120,7 @@ class StatisticsChart extends LitElement {
: {},
},
time: {
tooltipFormat: "datetimeseconds",
tooltipFormat: "datetime",
},
},
y: {
@ -157,6 +158,8 @@ class StatisticsChart extends LitElement {
hitRadius: 5,
},
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
};
}

View File

@ -212,7 +212,15 @@ export class DialogEnergyGridFlowSettings
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
${this.hass.localize("ui.common.cancel")}
</mwc-button>
<mwc-button @click=${this._save} slot="primaryAction">
<mwc-button
@click=${this._save}
.disabled=${!this._source[
this._params!.direction === "from"
? "stat_energy_from"
: "stat_energy_to"
]}
slot="primaryAction"
>
${this.hass.localize("ui.common.save")}
</mwc-button>
</ha-dialog>
@ -231,32 +239,42 @@ export class DialogEnergyGridFlowSettings
}
private _numberPriceChanged(ev: CustomEvent) {
this._source!.number_energy_price = Number(ev.detail.value);
this._source!.entity_energy_price = null;
this._costStat = null;
this._source = {
...this._source!,
number_energy_price: Number(ev.detail.value),
entity_energy_price: null,
};
}
private _priceStatChanged(ev: CustomEvent) {
this._costStat = ev.detail.value;
this._source!.entity_energy_price = null;
this._source!.number_energy_price = null;
this._source = {
...this._source!,
entity_energy_price: null,
number_energy_price: null,
};
}
private _priceEntityChanged(ev: CustomEvent) {
this._source!.entity_energy_price = ev.detail.value;
this._source!.number_energy_price = null;
this._costStat = null;
this._source = {
...this._source!,
entity_energy_price: ev.detail.value,
number_energy_price: null,
};
}
private _statisticChanged(ev: CustomEvent<{ value: string }>) {
this._source![
this._params!.direction === "from" ? "stat_energy_from" : "stat_energy_to"
] = ev.detail.value;
this._source![
this._params!.direction === "from"
this._source = {
...this._source!,
[this._params!.direction === "from"
? "stat_energy_from"
: "stat_energy_to"]: ev.detail.value,
[this._params!.direction === "from"
? "entity_energy_from"
: "entity_energy_to"
] = ev.detail.value;
: "entity_energy_to"]: ev.detail.value,
};
}
private async _save() {

View File

@ -140,7 +140,11 @@ export class DialogEnergySolarSettings
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
${this.hass.localize("ui.common.cancel")}
</mwc-button>
<mwc-button @click=${this._save} slot="primaryAction">
<mwc-button
@click=${this._save}
.disabled=${!this._source.stat_energy_from}
slot="primaryAction"
>
${this.hass.localize("ui.common.save")}
</mwc-button>
</ha-dialog>
@ -191,7 +195,7 @@ export class DialogEnergySolarSettings
}
private _statisticChanged(ev: CustomEvent<{ value: string }>) {
this._source!.stat_energy_from = ev.detail.value;
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
}
private async _save() {

View File

@ -61,15 +61,15 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
></ha-energy-device-settings>`}
<div class="buttons">
${this._step > 0
? html`<mwc-button @click=${this._back}
? html`<mwc-button outlined @click=${this._back}
>${this.hass.localize("ui.panel.energy.setup.back")}</mwc-button
>`
: html`<div></div>`}
${this._step < 2
? html`<mwc-button outlined @click=${this._next}
? html`<mwc-button unelevated @click=${this._next}
>${this.hass.localize("ui.panel.energy.setup.next")}</mwc-button
>`
: html`<mwc-button raised @click=${this._setupDone}>
: html`<mwc-button unelevated @click=${this._setupDone}>
${this.hass.localize("ui.panel.energy.setup.done")}
</mwc-button>`}
</div>

View File

@ -50,7 +50,7 @@ export class EnergyStrategy {
if (hasGrid) {
view.cards!.push({
title: "Energy usage",
type: "energy-summary-graph",
type: "energy-usage-graph",
prefs: energyPrefs,
});
}
@ -64,11 +64,10 @@ export class EnergyStrategy {
});
}
// Only include if we have a grid.
if (hasGrid) {
if (hasGrid || hasSolar) {
view.cards!.push({
title: "Costs",
type: "energy-costs-table",
title: "Sources",
type: "energy-sources-table",
prefs: energyPrefs,
});
}

View File

@ -105,7 +105,7 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
(totalSolarProduction || 0) -
(totalGridReturned || 0);
value = round((1 - highCarbonEnergy / totalEnergyConsumed) * 100);
value = round((highCarbonEnergy / totalEnergyConsumed) * 100);
}
return html`

View File

@ -1,272 +0,0 @@
// @ts-ignore
import dataTableStyles from "@material/data-table/dist/mdc.data-table.min.css";
import {
css,
CSSResultGroup,
html,
LitElement,
TemplateResult,
unsafeCSS,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { round } from "../../../../common/number/round";
import { formatNumber } from "../../../../common/string/format_number";
import "../../../../components/chart/statistics-chart";
import "../../../../components/ha-card";
import {
EnergyInfo,
getEnergyInfo,
GridSourceTypeEnergyPreference,
} from "../../../../data/energy";
import {
calculateStatisticSumGrowth,
fetchStatistics,
Statistics,
} from "../../../../data/history";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergyDevicesGraphCardConfig } from "../types";
@customElement("hui-energy-costs-table-card")
export class HuiEnergyCostsTableCard
extends LitElement
implements LovelaceCard
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: EnergyDevicesGraphCardConfig;
@state() private _stats?: Statistics;
@state() private _energyInfo?: EnergyInfo;
public getCardSize(): Promise<number> | number {
return 3;
}
public setConfig(config: EnergyDevicesGraphCardConfig): void {
this._config = config;
}
public willUpdate() {
if (!this.hasUpdated) {
this._getEnergyInfo().then(() => this._getStatistics());
}
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
if (!this._stats) {
return html`Loading...`;
}
const source = this._config.prefs.energy_sources?.find(
(src) => src.type === "grid"
) as GridSourceTypeEnergyPreference | undefined;
if (!source) {
return html`No grid source found.`;
}
let totalEnergy = 0;
let totalCost = 0;
return html` <ha-card .header="${this._config.title}">
<div class="mdc-data-table">
<div class="mdc-data-table__table-container">
<table class="mdc-data-table__table" aria-label="Dessert calories">
<thead>
<tr class="mdc-data-table__header-row">
<th
class="mdc-data-table__header-cell"
role="columnheader"
scope="col"
>
Grid source
</th>
<th
class="mdc-data-table__header-cell mdc-data-table__header-cell--numeric"
role="columnheader"
scope="col"
>
Energy
</th>
<th
class="mdc-data-table__header-cell mdc-data-table__header-cell--numeric"
role="columnheader"
scope="col"
>
Cost
</th>
</tr>
</thead>
<tbody class="mdc-data-table__content">
${source.flow_from.map((flow) => {
const entity = this.hass.states[flow.stat_energy_from];
const energy =
calculateStatisticSumGrowth(
this._stats![flow.stat_energy_from]
) || 0;
totalEnergy += energy;
const cost_stat =
flow.stat_cost ||
this._energyInfo!.cost_sensors[flow.stat_energy_from];
const cost =
(cost_stat &&
calculateStatisticSumGrowth(this._stats![cost_stat])) ||
0;
totalCost += cost;
return html`<tr class="mdc-data-table__row">
<th class="mdc-data-table__cell" scope="row">
${entity ? computeStateName(entity) : flow.stat_energy_from}
</th>
<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${round(energy)} kWh
</td>
<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(cost, this.hass.locale, {
style: "currency",
currency: this.hass.config.currency!,
})}
</td>
</tr>`;
})}
${source.flow_to.map((flow) => {
const entity = this.hass.states[flow.stat_energy_to];
const energy =
(calculateStatisticSumGrowth(
this._stats![flow.stat_energy_to]
) || 0) * -1;
totalEnergy += energy;
const cost_stat =
flow.stat_compensation ||
this._energyInfo!.cost_sensors[flow.stat_energy_to];
const cost =
((cost_stat &&
calculateStatisticSumGrowth(this._stats![cost_stat])) ||
0) * -1;
totalCost += cost;
return html`<tr class="mdc-data-table__row">
<th class="mdc-data-table__cell" scope="row">
${entity ? computeStateName(entity) : flow.stat_energy_to}
</th>
<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${round(energy)} kWh
</td>
<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(cost, this.hass.locale, {
style: "currency",
currency: this.hass.config.currency!,
})}
</td>
</tr>`;
})}
<tr class="mdc-data-table__row total">
<th class="mdc-data-table__cell" scope="row">Total</th>
<td class="mdc-data-table__cell mdc-data-table__cell--numeric">
${round(totalEnergy)} kWh
</td>
<td class="mdc-data-table__cell mdc-data-table__cell--numeric">
${formatNumber(totalCost, this.hass.locale, {
style: "currency",
currency: this.hass.config.currency!,
})}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</ha-card>`;
}
private async _getEnergyInfo() {
this._energyInfo = await getEnergyInfo(this.hass);
}
private async _getStatistics(): Promise<void> {
const startDate = new Date();
startDate.setHours(0, 0, 0, 0);
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
const statistics: string[] = Object.values(this._energyInfo!.cost_sensors);
const prefs = this._config!.prefs;
for (const source of prefs.energy_sources) {
if (source.type === "solar") {
continue;
}
// grid source
for (const flowFrom of source.flow_from) {
statistics.push(flowFrom.stat_energy_from);
if (flowFrom.stat_cost) {
statistics.push(flowFrom.stat_cost);
}
}
for (const flowTo of source.flow_to) {
statistics.push(flowTo.stat_energy_to);
if (flowTo.stat_compensation) {
statistics.push(flowTo.stat_compensation);
}
}
}
this._stats = await fetchStatistics(
this.hass!,
startDate,
undefined,
statistics
);
}
static get styles(): CSSResultGroup {
return css`
${unsafeCSS(dataTableStyles)}
.mdc-data-table {
width: 100%;
border: 0;
}
.mdc-data-table__header-cell,
.mdc-data-table__cell {
color: var(--primary-text-color);
border-bottom-color: var(--divider-color);
}
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
background-color: rgba(var(--rgb-primary-text-color), 0.04);
}
.total {
--mdc-typography-body2-font-weight: 500;
}
.total .mdc-data-table__cell {
border-top: 1px solid var(--divider-color);
}
ha-card {
height: 100%;
}
.content {
padding: 16px;
}
.has-header {
padding-top: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-energy-costs-table-card": HuiEnergyCostsTableCard;
}
}

View File

@ -16,6 +16,10 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { getColorByIndex } from "../../../../common/color/colors";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import {
formatNumber,
numberFormatToLocale,
} from "../../../../common/string/format_number";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import {
@ -106,7 +110,10 @@ export class HuiEnergyDevicesGraphCard
}
return html`
<ha-card .header="${this._config.title}">
<ha-card>
${this._config.title
? html`<h1 class="card-header">${this._config.title}</h1>`
: ""}
<div
class="content ${classMap({
"has-header": !!this._config.title,
@ -144,12 +151,15 @@ export class HuiEnergyDevicesGraphCard
mode: "nearest",
callbacks: {
label: (context) =>
`${context.dataset.label}: ${
Math.round(context.parsed.x * 100) / 100
} kWh`,
`${context.dataset.label}: ${formatNumber(
context.parsed.x,
this.hass.locale
)} kWh`,
},
},
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
};
}
@ -236,6 +246,9 @@ export class HuiEnergyDevicesGraphCard
ha-card {
height: 100%;
}
.card-header {
padding-bottom: 0;
}
.content {
padding: 16px;
}

View File

@ -10,7 +10,7 @@ import { css, html, LitElement, svg } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { round } from "../../../../common/number/round";
import { formatNumber } from "../../../../common/string/format_number";
import { subscribeOne } from "../../../../common/util/subscribe-one";
import "../../../../components/ha-card";
import "../../../../components/ha-svg-icon";
@ -140,13 +140,9 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
}
if (highCarbonConsumption !== null) {
const gridPctHighCarbon = highCarbonConsumption / totalConsumption;
lowCarbonConsumption = totalGridConsumption - highCarbonConsumption;
lowCarbonConsumption =
totalGridConsumption - totalGridConsumption * gridPctHighCarbon;
const homePctGridHighCarbon =
(gridPctHighCarbon * totalGridConsumption) / totalConsumption;
const homePctGridHighCarbon = highCarbonConsumption / totalConsumption;
homeHighCarbonCircumference =
CIRCLE_CIRCUMFERENCE * homePctGridHighCarbon;
@ -158,6 +154,15 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
}
}
homeSolarCircumference = CIRCLE_CIRCUMFERENCE * 0.1;
homeHighCarbonCircumference = CIRCLE_CIRCUMFERENCE * 0.8;
homeLowCarbonCircumference =
CIRCLE_CIRCUMFERENCE -
(homeSolarCircumference || 0) -
homeHighCarbonCircumference;
return html`
<ha-card .header=${this._config.title}>
<div class="card-content">
@ -165,8 +170,7 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
? html`<div class="row">
${lowCarbonConsumption === undefined
? html`<div class="spacer"></div>`
: html`
<div class="circle-container low-carbon">
: html`<div class="circle-container low-carbon">
<span class="label">Non-fossil</span>
<a
class="circle"
@ -175,19 +179,30 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
rel="noopener no referrer"
>
<ha-svg-icon .path="${mdiLeaf}"></ha-svg-icon>
${round(lowCarbonConsumption, 1)} kWh
${lowCarbonConsumption
? formatNumber(
lowCarbonConsumption,
this.hass.locale,
{ maximumFractionDigits: 1 }
)
: "-"}
kWh
</a>
<svg width="80" height="30">
<line x1="40" y1="0" x2="40" y2="30"></line>
</svg>
</div>
`}
</div>`}
${hasSolarProduction
? html`<div class="circle-container solar">
<span class="label">Solar</span>
<div class="circle">
<ha-svg-icon .path="${mdiSolarPower}"></ha-svg-icon>
${round(totalSolarProduction || 0, 1)} kWh
${formatNumber(
totalSolarProduction || 0,
this.hass.locale,
{ maximumFractionDigits: 1 }
)}
kWh
</div>
</div>`
: ""}
@ -204,7 +219,11 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
class="small"
.path=${mdiArrowRight}
></ha-svg-icon>`
: ""}${round(totalGridConsumption, 1)}
: ""}${formatNumber(
totalGridConsumption,
this.hass.locale,
{ maximumFractionDigits: 1 }
)}
kWh
</span>
${productionReturnedToGrid !== null
@ -213,7 +232,12 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
class="small"
.path=${mdiArrowLeft}
></ha-svg-icon
>${round(productionReturnedToGrid, 1)} kWh
>${formatNumber(
productionReturnedToGrid,
this.hass.locale,
{ maximumFractionDigits: 1 }
)}
kWh
</span>`
: ""}
</div>
@ -228,13 +252,15 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
})}"
>
<ha-svg-icon .path="${mdiHome}"></ha-svg-icon>
${round(totalConsumption, 1)} kWh
${formatNumber(totalConsumption, this.hass.locale, {
maximumFractionDigits: 1,
})}
kWh
${homeSolarCircumference !== undefined ||
homeLowCarbonCircumference !== undefined
? html`<svg>
${homeSolarCircumference !== undefined
? svg`
<circle
? svg`<circle
class="solar"
cx="40"
cy="40"
@ -243,23 +269,24 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
CIRCLE_CIRCUMFERENCE - homeSolarCircumference
}"
shape-rendering="geometricPrecision"
stroke-dashoffset="0"
stroke-dashoffset="-${
CIRCLE_CIRCUMFERENCE - homeSolarCircumference
}"
/>`
: ""}
${homeHighCarbonCircumference
? svg`
<circle
${homeLowCarbonCircumference
? svg`<circle
class="low-carbon"
cx="40"
cy="40"
r="38"
stroke-dasharray="${homeLowCarbonCircumference} ${
CIRCLE_CIRCUMFERENCE - homeLowCarbonCircumference!
CIRCLE_CIRCUMFERENCE - homeLowCarbonCircumference
}"
stroke-dashoffset="${
((homeSolarCircumference || 0) +
homeHighCarbonCircumference!) *
-1
stroke-dashoffset="-${
CIRCLE_CIRCUMFERENCE -
homeLowCarbonCircumference -
(homeSolarCircumference || 0)
}"
shape-rendering="geometricPrecision"
/>`
@ -271,11 +298,11 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
r="38"
stroke-dasharray="${homeHighCarbonCircumference ??
CIRCLE_CIRCUMFERENCE -
homeSolarCircumference!} ${homeHighCarbonCircumference
homeSolarCircumference!} ${homeHighCarbonCircumference !==
undefined
? CIRCLE_CIRCUMFERENCE - homeHighCarbonCircumference
: homeSolarCircumference}"
stroke-dashoffset="${(homeSolarCircumference || 0) *
-1}"
stroke-dashoffset="0"
shape-rendering="geometricPrecision"
/>
</svg>`
@ -315,7 +342,11 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
vector-effect="non-scaling-stroke"
></path>
${productionReturnedToGrid && hasSolarProduction
? svg`<circle r="1" class="return" vector-effect="non-scaling-stroke">
? svg`<circle
r="1"
class="return"
vector-effect="non-scaling-stroke"
>
<animateMotion
dur="${
6 -
@ -332,8 +363,11 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
</circle>`
: ""}
${totalSolarProduction
? svg`
<circle r="1" class="solar" vector-effect="non-scaling-stroke">
? svg`<circle
r="1"
class="solar"
vector-effect="non-scaling-stroke"
>
<animateMotion
dur="${
6 -
@ -351,7 +385,11 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
</circle>`
: ""}
${totalGridConsumption
? svg`<circle r="1" class="grid" vector-effect="non-scaling-stroke">
? svg`<circle
r="1"
class="grid"
vector-effect="non-scaling-stroke"
>
<animateMotion
dur="${
6 -
@ -471,12 +509,15 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
flex-direction: column;
align-items: center;
}
.circle-container.low-carbon {
margin-right: 4px;
}
.circle-container.solar {
margin-left: 4px;
height: 130px;
}
.spacer {
width: 80px;
height: 30px;
width: 84px;
}
.circle {
width: 80px;
@ -519,67 +560,70 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
height: 100%;
}
.low-carbon line {
stroke: #0f9d58;
stroke: var(--energy-non-fossil-color);
}
.low-carbon .circle {
border-color: #0f9d58;
border-color: var(--energy-non-fossil-color);
}
.low-carbon ha-svg-icon {
color: #0f9d58;
color: var(--energy-non-fossil-color);
}
circle.low-carbon {
stroke: var(--energy-non-fossil-color);
fill: var(--energy-non-fossil-color);
}
.solar .circle {
border-color: #ff9800;
border-color: var(--energy-solar-color);
}
circle.solar,
path.solar {
stroke: #ff9800;
stroke: var(--energy-solar-color);
}
circle.solar {
stroke-width: 4;
fill: #ff9800;
}
circle.low-carbon {
stroke: #0f9d58;
fill: #0f9d58;
fill: var(--energy-solar-color);
}
path.return,
circle.return {
stroke: #673ab7;
stroke: var(--energy-grid-return-color);
}
circle.return {
stroke-width: 4;
fill: #673ab7;
fill: var(--energy-grid-return-color);
}
.return {
color: #673ab7;
color: var(--energy-grid-return-color);
}
.grid .circle {
border-color: #126a9a;
border-color: var(--energy-grid-consumption-color);
}
.consumption {
color: #126a9a;
color: var(--energy-grid-consumption-color);
}
circle.grid,
path.grid {
stroke: #126a9a;
stroke: var(--energy-grid-consumption-color);
}
circle.grid {
stroke-width: 4;
fill: #126a9a;
fill: var(--energy-grid-consumption-color);
}
.home .circle {
border: none;
}
.home .circle.border {
border-width: 0;
border-color: var(--primary-color);
}
.home .circle.border {
border-width: 2px;
}
.circle svg circle {
animation: rotate-in 0.2s ease-in;
animation: rotate-in 0.6s ease-in;
transition: stroke-dashoffset 0.4s, stroke-dasharray 0.4s;
fill: none;
}
@keyframes rotate-in {
from {
stroke-dashoffset: 0;
stroke-dashoffset: 238.76104;
stroke-dasharray: 238.76104;
}
}
`;

View File

@ -1,7 +1,6 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { round } from "../../../../common/number/round";
import "../../../../components/ha-card";
import "../../../../components/ha-gauge";
import { energySourcesByType } from "../../../../data/energy";
@ -63,10 +62,14 @@ class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard {
let value: number | undefined;
if (productionReturnedToGrid !== null && totalSolarProduction !== null) {
const cosumedSolar = totalSolarProduction - productionReturnedToGrid;
value = round((cosumedSolar / totalSolarProduction) * 100);
if (productionReturnedToGrid !== null && totalSolarProduction) {
const cosumedSolar = Math.min(
0,
totalSolarProduction - productionReturnedToGrid
);
value = (cosumedSolar / totalSolarProduction) * 100;
}
return html`
<ha-card>
${value !== undefined
@ -81,7 +84,9 @@ class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard {
})}
></ha-gauge>
<div class="name">Self consumed solar energy</div>`
: html`Self consumed solar energy couldn't be calculated`}
: totalSolarProduction === 0
? "You have not produced any solar energy"
: "Self consumed solar energy couldn't be calculated"}
</ha-card>
`;
}

View File

@ -31,8 +31,10 @@ import { computeStateName } from "../../../../common/entity/compute_state_name";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-switch";
import "../../../../components/ha-formfield";
const SOLAR_COLOR = "#FF9800";
import {
formatNumber,
numberFormatToLocale,
} from "../../../../common/string/format_number";
@customElement("hui-energy-solar-graph-card")
export class HuiEnergySolarGraphCard
@ -119,7 +121,10 @@ export class HuiEnergySolarGraphCard
}
return html`
<ha-card .header="${this._config.title}">
<ha-card>
${this._config.title
? html`<h1 class="card-header">${this._config.title}</h1>`
: ""}
<div
class="content ${classMap({
"has-header": !!this._config.title,
@ -166,7 +171,7 @@ export class HuiEnergySolarGraphCard
: {},
},
time: {
tooltipFormat: "datetimeseconds",
tooltipFormat: "datetime",
},
offset: true,
},
@ -186,7 +191,10 @@ export class HuiEnergySolarGraphCard
mode: "nearest",
callbacks: {
label: (context) =>
`${context.dataset.label}: ${context.parsed.y} kWh`,
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
this.hass.locale
)} kWh`,
},
},
filler: {
@ -212,6 +220,8 @@ export class HuiEnergySolarGraphCard
hitRadius: 5,
},
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
};
}
@ -219,6 +229,7 @@ export class HuiEnergySolarGraphCard
if (this._fetching) {
return;
}
const startDate = new Date();
startDate.setHours(0, 0, 0, 0);
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
@ -273,20 +284,25 @@ export class HuiEnergySolarGraphCard
endTime = new Date();
}
const computedStyles = getComputedStyle(this);
const solarColor = computedStyles
.getPropertyValue("--energy-solar-color")
.trim();
solarSources.forEach((source, idx) => {
const data: ChartDataset<"bar" | "line">[] = [];
const entity = this.hass.states[source.stat_energy_from];
const borderColor =
idx > 0
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(SOLAR_COLOR)), idx)))
: SOLAR_COLOR;
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(solarColor)), idx)))
: solarColor;
data.push({
label: `Production ${
entity ? computeStateName(entity) : source.stat_energy_from
}`,
borderColor: borderColor,
borderColor,
backgroundColor: borderColor + "7F",
data: [],
});
@ -307,7 +323,7 @@ export class HuiEnergySolarGraphCard
if (prevStart === point.start) {
continue;
}
const value = Math.round((point.sum - prevValue) * 100) / 100;
const value = point.sum - prevValue;
const date = new Date(point.start);
data[0].data.push({
x: date.getTime(),
@ -347,7 +363,9 @@ export class HuiEnergySolarGraphCard
}`,
fill: false,
stepped: false,
borderColor: "#000",
borderColor: computedStyles.getPropertyValue(
"--primary-text-color"
),
borderDash: [7, 5],
pointRadius: 0,
data: [],
@ -386,6 +404,9 @@ export class HuiEnergySolarGraphCard
ha-card {
height: 100%;
}
.card-header {
padding-bottom: 0;
}
.content {
padding: 16px;
}

View File

@ -0,0 +1,426 @@
// @ts-ignore
import dataTableStyles from "@material/data-table/dist/mdc.data-table.min.css";
import {
css,
CSSResultGroup,
html,
LitElement,
TemplateResult,
unsafeCSS,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import {
rgb2hex,
lab2rgb,
rgb2lab,
hex2rgb,
} from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { formatNumber } from "../../../../common/string/format_number";
import "../../../../components/chart/statistics-chart";
import "../../../../components/ha-card";
import {
EnergyInfo,
energySourcesByType,
getEnergyInfo,
} from "../../../../data/energy";
import {
calculateStatisticSumGrowth,
fetchStatistics,
Statistics,
} from "../../../../data/history";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergySourcesTableCardConfig } from "../types";
@customElement("hui-energy-sources-table-card")
export class HuiEnergySourcesTableCard
extends LitElement
implements LovelaceCard
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: EnergySourcesTableCardConfig;
@state() private _stats?: Statistics;
@state() private _energyInfo?: EnergyInfo;
public getCardSize(): Promise<number> | number {
return 3;
}
public setConfig(config: EnergySourcesTableCardConfig): void {
this._config = config;
}
public willUpdate() {
if (!this.hasUpdated) {
this._getEnergyInfo().then(() => this._getStatistics());
}
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
if (!this._stats) {
return html`Loading...`;
}
let totalGrid = 0;
let totalSolar = 0;
let totalCost = 0;
const types = energySourcesByType(this._config.prefs);
const computedStyles = getComputedStyle(this);
const solarColor = computedStyles
.getPropertyValue("--energy-solar-color")
.trim();
const returnColor = computedStyles
.getPropertyValue("--energy-grid-return-color")
.trim();
const consumptionColor = computedStyles
.getPropertyValue("--energy-grid-consumption-color")
.trim();
const showCosts =
types.grid?.[0].flow_from.some(
(flow) =>
flow.stat_cost || flow.entity_energy_price || flow.number_energy_price
) ||
types.grid?.[0].flow_to.some(
(flow) =>
flow.stat_compensation ||
flow.entity_energy_price ||
flow.number_energy_price
);
return html` <ha-card>
${this._config.title
? html`<h1 class="card-header">${this._config.title}</h1>`
: ""}
<div class="mdc-data-table">
<div class="mdc-data-table__table-container">
<table class="mdc-data-table__table" aria-label="Dessert calories">
<thead>
<tr class="mdc-data-table__header-row">
<th class="mdc-data-table__header-cell"></th>
<th
class="mdc-data-table__header-cell"
role="columnheader"
scope="col"
>
Source
</th>
<th
class="mdc-data-table__header-cell mdc-data-table__header-cell--numeric"
role="columnheader"
scope="col"
>
Energy
</th>
${showCosts
? html` <th
class="mdc-data-table__header-cell mdc-data-table__header-cell--numeric"
role="columnheader"
scope="col"
>
Cost
</th>`
: ""}
</tr>
</thead>
<tbody class="mdc-data-table__content">
${types.solar?.map((source, idx) => {
const entity = this.hass.states[source.stat_energy_from];
const energy =
calculateStatisticSumGrowth(
this._stats![source.stat_energy_from]
) || 0;
totalSolar += energy;
const color =
idx > 0
? rgb2hex(
lab2rgb(labDarken(rgb2lab(hex2rgb(solarColor)), idx))
)
: solarColor;
return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet">
<div
class="bullet"
style=${styleMap({
borderColor: color,
backgroundColor: color + "7F",
})}
></div>
</td>
<th class="mdc-data-table__cell" scope="row">
${entity
? computeStateName(entity)
: source.stat_energy_from}
</th>
<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(energy, this.hass.locale)} kWh
</td>
${showCosts
? html`<td class="mdc-data-table__cell"></td>`
: ""}
</tr>`;
})}
${types.solar
? html`<tr class="mdc-data-table__row total">
<td class="mdc-data-table__cell"></td>
<th class="mdc-data-table__cell" scope="row">
Solar total
</th>
<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(totalSolar, this.hass.locale)} kWh
</td>
${showCosts
? html`<td class="mdc-data-table__cell"></td>`
: ""}
</tr>`
: ""}
${types.grid?.map(
(source) => html`${source.flow_from.map((flow, idx) => {
const entity = this.hass.states[flow.stat_energy_from];
const energy =
calculateStatisticSumGrowth(
this._stats![flow.stat_energy_from]
) || 0;
totalGrid += energy;
const cost_stat =
flow.stat_cost ||
this._energyInfo!.cost_sensors[flow.stat_energy_from];
const cost = cost_stat
? calculateStatisticSumGrowth(this._stats![cost_stat])
: null;
if (cost !== null) {
totalCost += cost;
}
const color =
idx > 0
? rgb2hex(
lab2rgb(
labDarken(rgb2lab(hex2rgb(consumptionColor)), idx)
)
)
: consumptionColor;
return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet">
<div
class="bullet"
style=${styleMap({
borderColor: color,
backgroundColor: color + "7F",
})}
></div>
</td>
<th class="mdc-data-table__cell" scope="row">
${entity
? computeStateName(entity)
: flow.stat_energy_from}
</th>
<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(energy, this.hass.locale)} kWh
</td>
${showCosts
? html` <td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${cost !== null
? formatNumber(cost, this.hass.locale, {
style: "currency",
currency: this.hass.config.currency!,
})
: ""}
</td>`
: ""}
</tr>`;
})}
${source.flow_to.map((flow, idx) => {
const entity = this.hass.states[flow.stat_energy_to];
const energy =
(calculateStatisticSumGrowth(
this._stats![flow.stat_energy_to]
) || 0) * -1;
totalGrid += energy;
const cost_stat =
flow.stat_compensation ||
this._energyInfo!.cost_sensors[flow.stat_energy_to];
const cost = cost_stat
? calculateStatisticSumGrowth(this._stats![cost_stat])
: null;
if (cost !== null) {
totalCost += cost;
}
const color =
idx > 0
? rgb2hex(
lab2rgb(labDarken(rgb2lab(hex2rgb(returnColor)), idx))
)
: returnColor;
return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet">
<div
class="bullet"
style=${styleMap({
borderColor: color,
backgroundColor: color + "7F",
})}
></div>
</td>
<th class="mdc-data-table__cell" scope="row">
${entity ? computeStateName(entity) : flow.stat_energy_to}
</th>
<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(energy, this.hass.locale)} kWh
</td>
${showCosts
? html` <td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${cost !== null
? formatNumber(cost, this.hass.locale, {
style: "currency",
currency: this.hass.config.currency!,
})
: ""}
</td>`
: ""}
</tr>`;
})}`
)}
${types.grid
? html` <tr class="mdc-data-table__row total">
<td class="mdc-data-table__cell"></td>
<th class="mdc-data-table__cell" scope="row">Grid total</th>
<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(totalGrid, this.hass.locale)} kWh
</td>
${showCosts
? html`<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(totalCost, this.hass.locale, {
style: "currency",
currency: this.hass.config.currency!,
})}
</td>`
: ""}
</tr>`
: ""}
</tbody>
</table>
</div>
</div>
</ha-card>`;
}
private async _getEnergyInfo() {
this._energyInfo = await getEnergyInfo(this.hass);
}
private async _getStatistics(): Promise<void> {
const startDate = new Date();
startDate.setHours(0, 0, 0, 0);
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
const statistics: string[] = Object.values(this._energyInfo!.cost_sensors);
const prefs = this._config!.prefs;
for (const source of prefs.energy_sources) {
if (source.type === "solar") {
statistics.push(source.stat_energy_from);
} else {
// grid source
for (const flowFrom of source.flow_from) {
statistics.push(flowFrom.stat_energy_from);
if (flowFrom.stat_cost) {
statistics.push(flowFrom.stat_cost);
}
}
for (const flowTo of source.flow_to) {
statistics.push(flowTo.stat_energy_to);
if (flowTo.stat_compensation) {
statistics.push(flowTo.stat_compensation);
}
}
}
}
this._stats = await fetchStatistics(
this.hass!,
startDate,
undefined,
statistics
);
}
static get styles(): CSSResultGroup {
return css`
${unsafeCSS(dataTableStyles)}
.mdc-data-table {
width: 100%;
border: 0;
}
.mdc-data-table__header-cell,
.mdc-data-table__cell {
color: var(--primary-text-color);
border-bottom-color: var(--divider-color);
}
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
background-color: rgba(var(--rgb-primary-text-color), 0.04);
}
.total {
--mdc-typography-body2-font-weight: 500;
}
.total .mdc-data-table__cell {
border-top: 1px solid var(--divider-color);
}
ha-card {
height: 100%;
}
.card-header {
padding-bottom: 0;
}
.content {
padding: 16px;
}
.has-header {
padding-top: 0;
}
.cell-bullet {
width: 32px;
padding-right: 0;
}
.bullet {
border-width: 1px;
border-style: solid;
border-radius: 4px;
height: 16px;
width: 32px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-energy-sources-table-card": HuiEnergySourcesTableCard;
}
}

View File

@ -9,39 +9,34 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import {
hex2rgb,
lab2rgb,
rgb2hex,
rgb2lab,
} from "../../../../common/color/convert-color";
import { hexBlend } from "../../../../common/color/hex";
import { labDarken } from "../../../../common/color/lab";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { round } from "../../../../common/number/round";
import { formatNumber } from "../../../../common/string/format_number";
import {
formatNumber,
numberFormatToLocale,
} from "../../../../common/string/format_number";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import { fetchStatistics, Statistics } from "../../../../data/history";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergySummaryGraphCardConfig } from "../types";
import { EnergyUsageGraphCardConfig } from "../types";
const NEGATIVE = ["to_grid"];
const COLORS = {
to_grid: { border: "#673ab7", background: "#b39bdb" },
from_grid: { border: "#126A9A", background: "#8ab5cd" },
used_solar: { border: "#FF9800", background: "#fecc8e" },
};
@customElement("hui-energy-summary-graph-card")
export class HuiEnergySummaryGraphCard
@customElement("hui-energy-usage-graph-card")
export class HuiEnergyUsageGraphCard
extends LitElement
implements LovelaceCard
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: EnergySummaryGraphCardConfig;
@state() private _config?: EnergyUsageGraphCardConfig;
@state() private _data?: Statistics;
@ -81,7 +76,7 @@ export class HuiEnergySummaryGraphCard
return 3;
}
public setConfig(config: EnergySummaryGraphCardConfig): void {
public setConfig(config: EnergyUsageGraphCardConfig): void {
this._config = config;
}
@ -95,7 +90,7 @@ export class HuiEnergySummaryGraphCard
}
const oldConfig = changedProps.get("_config") as
| EnergySummaryGraphCardConfig
| EnergyUsageGraphCardConfig
| undefined;
if (oldConfig !== this._config) {
@ -116,42 +111,14 @@ export class HuiEnergySummaryGraphCard
return html`
<ha-card>
<h1 class="card-header">${this._config.title}</h1>
${this._config.title
? html`<h1 class="card-header">${this._config.title}</h1>`
: ""}
<div
class="content ${classMap({
"has-header": !!this._config.title,
})}"
>
<div class="chartLegend">
<ul>
${this._chartData.datasets.map(
(dataset) => html`<li>
<div>
<div
class="bullet"
style=${styleMap({
backgroundColor: dataset.backgroundColor as string,
borderColor: dataset.borderColor as string,
})}
></div>
<span class="label">${dataset.label}</span>
</div>
<span class="value"
>${formatNumber(
Math.abs(
dataset.data.reduce(
(total, point) => total + (point as any).y,
0
) as number
),
this.hass.locale
)}
kWh</span
>
</li>`
)}
</ul>
</div>
<ha-chart-base
.data=${this._chartData}
.options=${this._chartOptions}
@ -193,7 +160,7 @@ export class HuiEnergySummaryGraphCard
: {},
},
time: {
tooltipFormat: "datetimeseconds",
tooltipFormat: "datetime",
},
offset: true,
},
@ -206,7 +173,8 @@ export class HuiEnergySummaryGraphCard
},
ticks: {
beginAtZero: true,
callback: (value) => Math.abs(round(value)),
callback: (value) =>
formatNumber(Math.abs(value), this.hass.locale),
},
},
},
@ -218,7 +186,10 @@ export class HuiEnergySummaryGraphCard
filter: (val) => val.formattedValue !== "0",
callbacks: {
label: (context) =>
`${context.dataset.label}: ${Math.abs(context.parsed.y)} kWh`,
`${context.dataset.label}: ${formatNumber(
Math.abs(context.parsed.y),
this.hass.locale
)} kWh`,
footer: (contexts) => {
let totalConsumed = 0;
let totalReturned = 0;
@ -233,10 +204,16 @@ export class HuiEnergySummaryGraphCard
}
return [
totalConsumed
? `Total consumed: ${totalConsumed.toFixed(2)} kWh`
? `Total consumed: ${formatNumber(
totalConsumed,
this.hass.locale
)} kWh`
: "",
totalReturned
? `Total returned: ${totalReturned.toFixed(2)} kWh`
? `Total returned: ${formatNumber(
totalReturned,
this.hass.locale
)} kWh`
: "",
].filter(Boolean);
},
@ -261,6 +238,8 @@ export class HuiEnergySummaryGraphCard
hitRadius: 5,
},
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
};
}
@ -344,6 +323,23 @@ export class HuiEnergySummaryGraphCard
} = {};
const summedData: { [key: string]: { [start: string]: number } } = {};
const computedStyles = getComputedStyle(this);
const colors = {
to_grid: computedStyles
.getPropertyValue("--energy-grid-return-color")
.trim(),
from_grid: computedStyles
.getPropertyValue("--energy-grid-consumption-color")
.trim(),
used_solar: computedStyles
.getPropertyValue("--energy-solar-color")
.trim(),
};
const backgroundColor = computedStyles
.getPropertyValue("--card-background-color")
.trim();
Object.entries(statistics).forEach(([key, statIds]) => {
const sum = ["solar", "to_grid"].includes(key);
const add = key !== "solar";
@ -407,12 +403,13 @@ export class HuiEnergySummaryGraphCard
const uniqueKeys = Array.from(new Set(allKeys));
Object.entries(combinedData).forEach(([type, sources]) => {
const negative = NEGATIVE.includes(type);
Object.entries(sources).forEach(([statId, source], idx) => {
const data: ChartDataset<"bar">[] = [];
const entity = this.hass.states[statId];
const color = COLORS[type];
const borderColor =
idx > 0
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(colors[type])), idx)))
: colors[type];
data.push({
label:
@ -421,28 +418,20 @@ export class HuiEnergySummaryGraphCard
: entity
? computeStateName(entity)
: statId,
borderColor:
idx > 0
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(color.border)), idx)))
: color.border,
backgroundColor:
idx > 0
? rgb2hex(
lab2rgb(labDarken(rgb2lab(hex2rgb(color.background)), idx))
)
: color.background,
borderColor,
backgroundColor: hexBlend(borderColor, backgroundColor, 50),
stack: "stack",
data: [],
});
// Process chart data.
for (const key of uniqueKeys) {
const value = key in source ? Math.round(source[key] * 100) / 100 : 0;
const value = source[key] || 0;
const date = new Date(key);
// @ts-expect-error
data[0].data.push({
x: date.getTime(),
y: value && negative ? -1 * value : value,
y: value && type === "to_grid" ? -1 * value : value,
});
}
@ -470,43 +459,12 @@ export class HuiEnergySummaryGraphCard
.has-header {
padding-top: 0;
}
.chartLegend ul {
padding-left: 20px;
}
.chartLegend li {
padding: 2px 8px;
display: flex;
justify-content: space-between;
align-items: center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
color: var(--secondary-text-color);
}
.chartLegend li > div {
display: flex;
align-items: center;
}
.chartLegend .bullet {
border-width: 1px;
border-style: solid;
border-radius: 4px;
display: inline-block;
height: 16px;
margin-right: 6px;
width: 32px;
box-sizing: border-box;
}
.value {
font-weight: 300;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-energy-summary-graph-card": HuiEnergySummaryGraphCard;
"hui-energy-usage-graph-card": HuiEnergyUsageGraphCard;
}
}

View File

@ -101,7 +101,7 @@ export interface EnergyDistributionCardConfig extends LovelaceCardConfig {
title?: string;
prefs: EnergyPreferences;
}
export interface EnergySummaryGraphCardConfig extends LovelaceCardConfig {
export interface EnergyUsageGraphCardConfig extends LovelaceCardConfig {
type: "energy-summary-graph";
title?: string;
prefs: EnergyPreferences;
@ -119,6 +119,12 @@ export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig {
prefs: EnergyPreferences;
}
export interface EnergySourcesTableCardConfig extends LovelaceCardConfig {
type: "energy-sources-table";
title?: string;
prefs: EnergyPreferences;
}
export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig {
type: "energy-solar-consumed-gauge";
title?: string;

View File

@ -35,14 +35,14 @@ const LAZY_LOAD_TYPES = {
"alarm-panel": () => import("../cards/hui-alarm-panel-card"),
error: () => import("../cards/hui-error-card"),
"empty-state": () => import("../cards/hui-empty-state-card"),
"energy-summary-graph": () =>
import("../cards/energy/hui-energy-summary-graph-card"),
"energy-usage-graph": () =>
import("../cards/energy/hui-energy-usage-graph-card"),
"energy-solar-graph": () =>
import("../cards/energy/hui-energy-solar-graph-card"),
"energy-devices-graph": () =>
import("../cards/energy/hui-energy-devices-graph-card"),
"energy-costs-table": () =>
import("../cards/energy/hui-energy-costs-table-card"),
"energy-sources-table": () =>
import("../cards/energy/hui-energy-sources-table-card"),
"energy-distribution": () =>
import("../cards/energy/hui-energy-distribution-card"),
"energy-solar-consumed-gauge": () =>

View File

@ -82,6 +82,14 @@ documentContainer.innerHTML = `<custom-style>
--state-climate-dry-color: #efbd07;
--state-climate-idle-color: #8a8a8a;
/* energy */
--energy-grid-consumption-color: #126a9a;
--energy-grid-return-color: #673ab7;
--energy-solar-color: #ff9800;
--energy-non-fossil-color: #0f9d58;
--rgb-energy-solar-color: 255, 152, 0;
/*
Paper-styles color.html dependency is stripped on build.
When a default paper-style color is used, it needs to be copied

View File

@ -31,6 +31,7 @@ export const darkStyles = {
"codemirror-property": "#C792EA",
"codemirror-qualifier": "#DECB6B",
"codemirror-type": "#DECB6B",
"energy-grid-return-color": "#b39bdb",
};
export const derivedStyles = {