Update energy dashboard (#9624)

This commit is contained in:
Bram Kragten 2021-07-27 22:57:09 +02:00 committed by GitHub
parent 0c0091375c
commit 73b9b87ef3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 662 additions and 456 deletions

View File

@ -272,7 +272,7 @@ export default class HaChartBase extends LitElement {
border-radius: 50%;
display: inline-block;
height: 16px;
margin-right: 4px;
margin-right: 6px;
width: 16px;
flex-shrink: 0;
box-sizing: border-box;
@ -280,9 +280,10 @@ export default class HaChartBase extends LitElement {
.chartTooltip .bullet {
align-self: baseline;
}
:host([rtl]) .chartLegend .bullet,
:host([rtl]) .chartTooltip .bullet {
margin-right: inherit;
margin-left: 4px;
margin-left: 6px;
}
.chartTooltip {
padding: 8px;
@ -314,6 +315,7 @@ export default class HaChartBase extends LitElement {
white-space: pre-line;
align-items: center;
line-height: 16px;
padding: 4px 0;
}
.chartTooltip .title {
text-align: center;

View File

@ -85,7 +85,8 @@ export class EnergyStrategy {
// Only include if we have a grid.
if (hasGrid) {
view.cards!.push({
type: "energy-usage",
title: "Energy distribution",
type: "energy-distribution",
prefs: energyPrefs,
view_layout: { position: "sidebar" },
});

View File

@ -1,23 +1,23 @@
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 { subscribeOne } from "../../../common/util/subscribe-one";
import "../../../components/ha-card";
import "../../../components/ha-gauge";
import { getConfigEntries } from "../../../data/config_entries";
import { energySourcesByType } from "../../../data/energy";
import { subscribeEntityRegistry } from "../../../data/entity_registry";
import { round } from "../../../../common/number/round";
import { subscribeOne } from "../../../../common/util/subscribe-one";
import "../../../../components/ha-card";
import "../../../../components/ha-gauge";
import { getConfigEntries } from "../../../../data/config_entries";
import { energySourcesByType } from "../../../../data/energy";
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
import {
calculateStatisticsSumGrowth,
fetchStatistics,
Statistics,
} from "../../../data/history";
import type { HomeAssistant } from "../../../types";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard } from "../types";
import { severityMap } from "./hui-gauge-card";
import type { EnergyCarbonGaugeCardConfig } from "./types";
} from "../../../../data/history";
import type { HomeAssistant } from "../../../../types";
import { createEntityNotFoundWarning } from "../../components/hui-warning";
import type { LovelaceCard } from "../../types";
import { severityMap } from "../hui-gauge-card";
import type { EnergyCarbonGaugeCardConfig } from "../types";
@customElement("hui-energy-carbon-consumed-gauge-card")
class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
@ -103,7 +103,7 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
(totalSolarProduction || 0) -
(totalGridReturned || 0);
value = round((highCarbonEnergy / totalEnergyConsumed) * 100);
value = round((1 - highCarbonEnergy / totalEnergyConsumed) * 100);
}
return html`
@ -116,23 +116,23 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
.locale=${this.hass!.locale}
label="%"
style=${styleMap({
"--gauge-color": this._computeSeverity(64),
"--gauge-color": this._computeSeverity(value),
})}
></ha-gauge>
<div class="name">High-carbon energy consumed</div>`
: html`Consumed high-carbon energy couldn't be calculated`}
<div class="name">Non-fossil energy consumed</div>`
: html`Consumed non-fossil energy couldn't be calculated`}
</ha-card>
`;
}
private _computeSeverity(numberValue: number): string {
if (numberValue > 50) {
if (numberValue < 10) {
return severityMap.red;
}
if (numberValue > 30) {
if (numberValue < 30) {
return severityMap.yellow;
}
if (numberValue < 10) {
if (numberValue > 75) {
return severityMap.green;
}
return severityMap.normal;

View File

@ -9,23 +9,23 @@ import {
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 "../../../components/chart/statistics-chart";
import "../../../components/ha-card";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { round } from "../../../../common/number/round";
import "../../../../components/chart/statistics-chart";
import "../../../../components/ha-card";
import {
EnergyInfo,
getEnergyInfo,
GridSourceTypeEnergyPreference,
} from "../../../data/energy";
} from "../../../../data/energy";
import {
calculateStatisticSumGrowth,
fetchStatistics,
Statistics,
} from "../../../data/history";
import { HomeAssistant } from "../../../types";
import { LovelaceCard } from "../types";
import { EnergyDevicesGraphCardConfig } from "./types";
} from "../../../../data/history";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergyDevicesGraphCardConfig } from "../types";
@customElement("hui-energy-costs-table-card")
export class HuiEnergyCostsTableCard

View File

@ -14,18 +14,18 @@ import {
} from "lit";
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 "../../../components/chart/ha-chart-base";
import "../../../components/ha-card";
import { getColorByIndex } from "../../../../common/color/colors";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import {
calculateStatisticSumGrowth,
fetchStatistics,
Statistics,
} from "../../../data/history";
import { HomeAssistant } from "../../../types";
import { LovelaceCard } from "../types";
import { EnergyDevicesGraphCardConfig } from "./types";
} from "../../../../data/history";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergyDevicesGraphCardConfig } from "../types";
@customElement("hui-energy-devices-graph-card")
export class HuiEnergyDevicesGraphCard

View File

@ -0,0 +1,505 @@
import {
mdiArrowLeft,
mdiArrowRight,
mdiHome,
mdiLeaf,
mdiSolarPower,
mdiTransmissionTower,
} from "@mdi/js";
import { css, html, LitElement, svg } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { round } from "../../../../common/number/round";
import { subscribeOne } from "../../../../common/util/subscribe-one";
import "../../../../components/ha-card";
import "../../../../components/ha-svg-icon";
import { getConfigEntries } from "../../../../data/config_entries";
import { energySourcesByType } from "../../../../data/energy";
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
import {
calculateStatisticsSumGrowth,
fetchStatistics,
Statistics,
} from "../../../../data/history";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergyDistributionCardConfig } from "../types";
const CIRCLE_CIRCUMFERENCE = 238.76104;
@customElement("hui-energy-distribution-card")
class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: EnergyDistributionCardConfig;
@state() private _stats?: Statistics;
@state() private _co2SignalEntity?: string;
private _fetching = false;
public setConfig(config: EnergyDistributionCardConfig): void {
this._config = config;
}
public getCardSize(): Promise<number> | number {
return 3;
}
public willUpdate(changedProps) {
super.willUpdate(changedProps);
if (!this._fetching && !this._stats) {
this._fetching = true;
Promise.all([this._getStatistics(), this._fetchCO2SignalEntity()]).then(
() => {
this._fetching = false;
}
);
}
}
protected render() {
if (!this._config) {
return html``;
}
if (!this._stats) {
return html`Loading…`;
}
const prefs = this._config!.prefs;
const types = energySourcesByType(prefs);
// The strategy only includes this card if we have a grid.
const hasConsumption = true;
const hasSolarProduction = types.solar !== undefined;
const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0;
const totalGridConsumption =
calculateStatisticsSumGrowth(
this._stats,
types.grid![0].flow_from.map((flow) => flow.stat_energy_from)
) ?? 0;
let totalSolarProduction: number | null = null;
if (hasSolarProduction) {
totalSolarProduction = calculateStatisticsSumGrowth(
this._stats,
types.solar!.map((source) => source.stat_energy_from)
);
}
let productionReturnedToGrid: number | null = null;
if (hasReturnToGrid) {
productionReturnedToGrid = calculateStatisticsSumGrowth(
this._stats,
types.grid![0].flow_to.map((flow) => flow.stat_energy_to)
);
}
// total consumption = consumption_from_grid + solar_production - return_to_grid
let co2percentage: number | undefined;
if (this._co2SignalEntity) {
const co2State = this.hass.states[this._co2SignalEntity];
if (co2State) {
co2percentage = Number(co2State.state);
if (isNaN(co2percentage)) {
co2percentage = undefined;
}
}
}
const totalConsumption =
totalGridConsumption +
(totalSolarProduction || 0) -
(productionReturnedToGrid || 0);
let homeSolarCircumference: number | undefined;
if (hasSolarProduction) {
const homePctSolar =
((totalSolarProduction || 0) - (productionReturnedToGrid || 0)) /
totalConsumption;
homeSolarCircumference = CIRCLE_CIRCUMFERENCE * homePctSolar;
}
let lowCarbonConsumption: number | undefined;
let homeLowCarbonCircumference: number | undefined;
let homeHighCarbonCircumference: number | undefined;
if (co2percentage !== undefined) {
const gridPctHighCarbon = co2percentage / 100;
lowCarbonConsumption =
totalGridConsumption - totalGridConsumption * gridPctHighCarbon;
const homePctGridHighCarbon =
(gridPctHighCarbon * totalGridConsumption) / totalConsumption;
homeHighCarbonCircumference =
CIRCLE_CIRCUMFERENCE * homePctGridHighCarbon;
homeLowCarbonCircumference =
CIRCLE_CIRCUMFERENCE -
(homeSolarCircumference || 0) -
homeHighCarbonCircumference;
}
return html`
<ha-card .header=${this._config.title}>
<div class="card-content">
${lowCarbonConsumption !== undefined || hasSolarProduction
? html`<div class="row">
${lowCarbonConsumption === undefined
? html`<div class="spacer"></div>`
: html`
<div class="circle-container low-carbon">
<span class="label">Non-fossil</span>
<div class="circle">
<ha-svg-icon .path="${mdiLeaf}"></ha-svg-icon>
${round(lowCarbonConsumption, 1)} kWh
</div>
<svg width="80" height="30">
<line x1="40" y1="0" x2="40" y2="30"></line>
</svg>
</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
</div>
</div>`
: ""}
<div class="spacer"></div>
</div>`
: ""}
<div class="row">
<div class="circle-container grid">
<div class="circle">
<ha-svg-icon .path="${mdiTransmissionTower}"></ha-svg-icon>
<span class="consumption">
${hasReturnToGrid
? html`<ha-svg-icon
class="small"
.path=${mdiArrowRight}
></ha-svg-icon>`
: ""}${round(totalGridConsumption, 1)}
kWh
</span>
${productionReturnedToGrid
? html`<span class="return">
<ha-svg-icon
class="small"
.path=${mdiArrowLeft}
></ha-svg-icon
>${round(productionReturnedToGrid, 1)} kWh
</span>`
: ""}
</div>
<span class="label">Grid</span>
</div>
<div class="circle-container home">
<div
class="circle ${classMap({
border:
homeSolarCircumference === undefined &&
homeLowCarbonCircumference === undefined,
})}"
>
<ha-svg-icon .path="${mdiHome}"></ha-svg-icon>
${round(totalConsumption, 1)} kWh
${homeSolarCircumference !== undefined ||
homeLowCarbonCircumference !== undefined
? html`<svg>
${homeSolarCircumference !== undefined
? svg`
<circle
class="solar"
cx="40"
cy="40"
r="38"
stroke-dasharray="${homeSolarCircumference} ${
CIRCLE_CIRCUMFERENCE - homeSolarCircumference
}"
shape-rendering="geometricPrecision"
stroke-dashoffset="0"
/>`
: ""}
${homeHighCarbonCircumference
? svg`
<circle
class="low-carbon"
cx="40"
cy="40"
r="38"
stroke-dasharray="${homeLowCarbonCircumference} ${
CIRCLE_CIRCUMFERENCE - homeLowCarbonCircumference!
}"
stroke-dashoffset="${
((homeSolarCircumference || 0) +
homeHighCarbonCircumference!) *
-1
}"
shape-rendering="geometricPrecision"
/>`
: ""}
<circle
class="grid"
cx="40"
cy="40"
r="38"
stroke-dasharray="${homeHighCarbonCircumference ??
CIRCLE_CIRCUMFERENCE -
homeSolarCircumference!} ${homeHighCarbonCircumference
? CIRCLE_CIRCUMFERENCE - homeHighCarbonCircumference
: homeSolarCircumference}"
stroke-dashoffset="${(homeSolarCircumference || 0) *
-1}"
shape-rendering="geometricPrecision"
/>
</svg>`
: ""}
</div>
<span class="label">Home</span>
</div>
</div>
<div class="lines">
<svg
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="none"
>
${productionReturnedToGrid && hasSolarProduction
? svg`<path
class="return"
d="M50,0 v20 c0,40 -10,35 -65,35 h20"
vector-effect="non-scaling-stroke"
></path>`
: ""}
${totalSolarProduction
? svg`<path
class="solar"
d="M50,0 v20 c0,40 10,35 65,35 h20"
vector-effect="non-scaling-stroke"
></path>`
: ""}
${totalGridConsumption
? svg`<path
class="grid"
d="M0,55 H100"
vector-effect="non-scaling-stroke"
></path>`
: ""}
</svg>
</div>
</div>
</ha-card>
`;
}
private async _fetchCO2SignalEntity() {
const [configEntries, entityRegistryEntries] = await Promise.all([
getConfigEntries(this.hass),
subscribeOne(this.hass.connection, subscribeEntityRegistry),
]);
const co2ConfigEntry = configEntries.find(
(entry) => entry.domain === "co2signal"
);
if (!co2ConfigEntry) {
return;
}
for (const entry of entityRegistryEntries) {
if (entry.config_entry_id !== co2ConfigEntry.entry_id) {
continue;
}
// The integration offers 2 entities. We want the % one.
const co2State = this.hass.states[entry.entity_id];
if (!co2State || co2State.attributes.unit_of_measurement !== "%") {
continue;
}
this._co2SignalEntity = co2State.entity_id;
break;
}
}
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[] = [];
const prefs = this._config!.prefs;
for (const source of prefs.energy_sources) {
if (source.type === "solar") {
statistics.push(source.stat_energy_from);
continue;
}
// grid source
for (const flowFrom of source.flow_from) {
statistics.push(flowFrom.stat_energy_from);
}
for (const flowTo of source.flow_to) {
statistics.push(flowTo.stat_energy_to);
}
}
this._stats = await fetchStatistics(
this.hass!,
startDate,
undefined,
statistics
);
}
static styles = css`
:host {
--mdc-icon-size: 24px;
}
.card-content {
position: relative;
}
.lines {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 146px;
display: flex;
justify-content: center;
padding: 0 16px 16px;
box-sizing: border-box;
}
.lines svg {
width: calc(100% - 160px);
height: 100%;
max-width: 340px;
}
.row {
display: flex;
justify-content: space-between;
max-width: 500px;
margin: 0 auto;
}
.circle-container {
display: flex;
flex-direction: column;
align-items: center;
}
.circle-container.solar {
height: 130px;
}
.spacer {
width: 80px;
height: 30px;
}
.circle {
width: 80px;
height: 80px;
border-radius: 50%;
box-sizing: border-box;
border: 2px solid;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
font-size: 12px;
line-height: 12px;
position: relative;
}
ha-svg-icon {
padding-bottom: 2px;
}
ha-svg-icon.small {
--mdc-icon-size: 12px;
}
.label {
color: var(--secondary-text-color);
font-size: 12px;
}
line,
path {
stroke: var(--primary-text-color);
stroke-width: 1;
fill: none;
}
.circle svg {
position: absolute;
fill: none;
stroke-width: 4px;
width: 100%;
height: 100%;
}
.circle svg circle {
animation: rotate-in 0.2s ease-in;
}
.low-carbon line {
stroke: #0f9d58;
}
.low-carbon .circle {
border-color: #0f9d58;
}
.low-carbon ha-svg-icon {
color: #0f9d58;
}
.solar .circle {
border-color: #ff9800;
}
path.solar,
circle.solar {
stroke: #ff9800;
}
circle.low-carbon {
stroke: #0f9d58;
}
circle.return,
path.return {
stroke: #673ab7;
}
.return {
color: #673ab7;
}
.grid .circle {
border-color: #126a9a;
}
.consumption {
color: #126a9a;
}
circle.grid,
path.grid {
stroke: #126a9a;
}
.home .circle {
border: none;
}
.home .circle.border {
border-color: var(--primary-color);
}
@keyframes rotate-in {
from {
stroke-dashoffset: 0;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hui-energy-distribution-card": HuiEnergyDistrubutionCard;
}
}

View File

@ -1,19 +1,19 @@
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";
import { round } from "../../../../common/number/round";
import "../../../../components/ha-card";
import "../../../../components/ha-gauge";
import { energySourcesByType } from "../../../../data/energy";
import {
calculateStatisticsSumGrowth,
fetchStatistics,
Statistics,
} from "../../../data/history";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCard } from "../types";
import { severityMap } from "./hui-gauge-card";
import type { EnergySolarGaugeCardConfig } from "./types";
} from "../../../../data/history";
import type { HomeAssistant } from "../../../../types";
import type { LovelaceCard } from "../../types";
import { severityMap } from "../hui-gauge-card";
import type { EnergySolarGaugeCardConfig } from "../types";
@customElement("hui-energy-solar-consumed-gauge-card")
class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard {
@ -77,7 +77,7 @@ class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard {
.locale=${this.hass!.locale}
label="%"
style=${styleMap({
"--gauge-color": this._computeSeverity(64),
"--gauge-color": this._computeSeverity(value),
})}
></ha-gauge>
<div class="name">Self consumed solar energy</div>`
@ -87,9 +87,12 @@ class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard {
}
private _computeSeverity(numberValue: number): string {
if (numberValue > 50) {
if (numberValue > 75) {
return severityMap.green;
}
if (numberValue < 50) {
return severityMap.yellow;
}
return severityMap.normal;
}

View File

@ -8,31 +8,31 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../components/ha-card";
import "../../../../components/ha-card";
import { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { HomeAssistant } from "../../../types";
import { LovelaceCard } from "../types";
import { EnergySolarGraphCardConfig } from "./types";
import { fetchStatistics, Statistics } from "../../../data/history";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergySolarGraphCardConfig } from "../types";
import { fetchStatistics, Statistics } from "../../../../data/history";
import {
hex2rgb,
lab2rgb,
rgb2hex,
rgb2lab,
} from "../../../common/color/convert-color";
import { labDarken } from "../../../common/color/lab";
import { SolarSourceTypeEnergyPreference } from "../../../data/energy";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
} from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab";
import { SolarSourceTypeEnergyPreference } from "../../../../data/energy";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import {
ForecastSolarForecast,
getForecastSolarForecasts,
} from "../../../data/forecast_solar";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/chart/ha-chart-base";
import "../../../components/ha-switch";
import "../../../components/ha-formfield";
} from "../../../../data/forecast_solar";
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 = { border: "#FF9800", background: "#ffcb80" };
const SOLAR_COLOR = "#FF9800";
@customElement("hui-energy-solar-graph-card")
export class HuiEnergySolarGraphCard
@ -123,17 +123,11 @@ export class HuiEnergySolarGraphCard
"has-header": !!this._config.title,
})}"
>
<ha-formfield label="Show all forecast data"
><ha-switch
.checked=${this._showAllForecastData}
@change=${this._showAllForecastChanged}
></ha-switch
></ha-formfield>
${this._chartData
? html`<ha-chart-base
.data=${this._chartData}
.options=${this._chartOptions}
chart-type="line"
chart-type="bar"
></ha-chart-base>`
: ""}
</div>
@ -142,12 +136,18 @@ export class HuiEnergySolarGraphCard
}
private _createOptions() {
const startDate = new Date();
startDate.setHours(0, 0, 0, 0);
const startTime = startDate.getTime();
this._chartOptions = {
parsing: false,
animation: false,
scales: {
x: {
type: "time",
suggestedMin: startTime,
suggestedMax: startTime + 24 * 60 * 60 * 1000,
adapters: {
date: {
locale: this.hass.locale,
@ -168,9 +168,14 @@ export class HuiEnergySolarGraphCard
time: {
tooltipFormat: "datetimeseconds",
},
offset: true,
},
y: {
type: "linear",
title: {
display: true,
text: "kWh",
},
ticks: {
beginAtZero: true,
},
@ -199,9 +204,10 @@ export class HuiEnergySolarGraphCard
},
elements: {
line: {
tension: 0.4,
tension: 0.3,
borderWidth: 1.5,
},
bar: { borderWidth: 1.5 },
point: {
hitRadius: 5,
},
@ -252,7 +258,7 @@ export class HuiEnergySolarGraphCard
) as SolarSourceTypeEnergyPreference[];
const statisticsData = Object.values(this._data!);
const datasets: ChartDataset<"line">[] = [];
const datasets: ChartDataset<"bar">[] = [];
let endTime: Date;
if (statisticsData.length === 0) {
@ -272,22 +278,18 @@ export class HuiEnergySolarGraphCard
}
solarSources.forEach((source, idx) => {
const data: ChartDataset<"line">[] = [];
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.border)), idx))
)
: SOLAR_COLOR.border;
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(SOLAR_COLOR)), idx)))
: SOLAR_COLOR;
data.push({
label: `Production ${
entity ? computeStateName(entity) : source.stat_energy_from
}`,
fill: true,
stepped: false,
borderColor: borderColor,
backgroundColor: borderColor + "7F",
data: [],
@ -343,6 +345,7 @@ export class HuiEnergySolarGraphCard
if (forecastsData) {
const forecast: ChartDataset<"line"> = {
type: "line",
label: `Forecast ${
entity ? computeStateName(entity) : source.stat_energy_from
}`,
@ -382,11 +385,6 @@ export class HuiEnergySolarGraphCard
};
}
private _showAllForecastChanged(ev) {
this._showAllForecastData = ev.target.checked;
this._renderChart();
}
static get styles(): CSSResultGroup {
return css`
ha-card {

View File

@ -1,21 +1,21 @@
import { mdiCashMultiple, mdiSolarPower } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-svg-icon";
import "../../../../components/ha-svg-icon";
import {
energySourcesByType,
GridSourceTypeEnergyPreference,
SolarSourceTypeEnergyPreference,
} from "../../../data/energy";
} from "../../../../data/energy";
import {
calculateStatisticSumGrowth,
fetchStatistics,
Statistics,
} from "../../../data/history";
import { HomeAssistant } from "../../../types";
import { LovelaceCard } from "../types";
import { EnergySummaryCardConfig } from "./types";
import "../../../components/ha-card";
} from "../../../../data/history";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergySummaryCardConfig } from "../types";
import "../../../../components/ha-card";
const renderSumStatHelper = (
data: Statistics,

View File

@ -8,33 +8,28 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../components/ha-card";
import "../../../../components/ha-card";
import { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { HomeAssistant } from "../../../types";
import { LovelaceCard } from "../types";
import { EnergySummaryGraphCardConfig } from "./types";
import { fetchStatistics, Statistics } from "../../../data/history";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergySummaryGraphCardConfig } from "../types";
import { fetchStatistics, Statistics } from "../../../../data/history";
import {
hex2rgb,
lab2rgb,
rgb2hex,
rgb2lab,
} from "../../../common/color/convert-color";
import { labDarken } from "../../../common/color/lab";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/chart/ha-chart-base";
import { round } from "../../../common/number/round";
} from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import "../../../../components/chart/ha-chart-base";
import { round } from "../../../../common/number/round";
const NEGATIVE = ["to_grid"];
const ORDER = {
used_solar: 0,
from_grid: 100,
to_grid: 200,
};
const COLORS = {
to_grid: { border: "#56d256", background: "#87ceab" },
from_grid: { border: "#126A9A", background: "#88b5cd" },
used_solar: { border: "#FF9800", background: "#ffcb80" },
to_grid: { border: "#673ab7", background: "#b39bdb" },
from_grid: { border: "#126A9A", background: "#8ab5cd" },
used_solar: { border: "#FF9800", background: "#fecc8e" },
};
@customElement("hui-energy-summary-graph-card")
@ -126,7 +121,7 @@ export class HuiEnergySummaryGraphCard
? html`<ha-chart-base
.data=${this._chartData}
.options=${this._chartOptions}
chartType="line"
chart-type="bar"
></ha-chart-base>`
: ""}
</div>
@ -135,12 +130,18 @@ export class HuiEnergySummaryGraphCard
}
private _createOptions() {
const startDate = new Date();
startDate.setHours(0, 0, 0, 0);
const startTime = startDate.getTime();
this._chartOptions = {
parsing: false,
animation: false,
scales: {
x: {
type: "time",
suggestedMin: startTime,
suggestedMax: startTime + 24 * 60 * 60 * 1000,
adapters: {
date: {
locale: this.hass.locale,
@ -161,10 +162,15 @@ export class HuiEnergySummaryGraphCard
time: {
tooltipFormat: "datetimeseconds",
},
offset: true,
},
y: {
stacked: true,
type: "linear",
title: {
display: true,
text: "kWh",
},
ticks: {
beginAtZero: true,
callback: (value) => Math.abs(round(value)),
@ -193,9 +199,13 @@ export class HuiEnergySummaryGraphCard
}
}
return [
`Total consumed: ${totalConsumed.toFixed(2)} kWh`,
`Total returned: ${totalReturned.toFixed(2)} kWh`,
];
totalConsumed
? `Total consumed: ${totalConsumed.toFixed(2)} kWh`
: "",
totalReturned
? `Total returned: ${totalReturned.toFixed(2)} kWh`
: "",
].filter(Boolean);
},
},
},
@ -213,10 +223,7 @@ export class HuiEnergySummaryGraphCard
mode: "nearest",
},
elements: {
line: {
tension: 0.4,
borderWidth: 1.5,
},
bar: { borderWidth: 1.5 },
point: {
hitRadius: 5,
},
@ -280,7 +287,7 @@ export class HuiEnergySummaryGraphCard
}
const statisticsData = Object.values(this._data!);
const datasets: ChartDataset<"line">[] = [];
const datasets: ChartDataset<"bar">[] = [];
let endTime: Date;
if (statisticsData.length === 0) {
@ -370,7 +377,7 @@ export class HuiEnergySummaryGraphCard
const negative = NEGATIVE.includes(type);
Object.entries(sources).forEach(([statId, source], idx) => {
const data: ChartDataset<"line">[] = [];
const data: ChartDataset<"bar">[] = [];
const entity = this.hass.states[statId];
const color = COLORS[type];
@ -381,9 +388,6 @@ export class HuiEnergySummaryGraphCard
: entity
? computeStateName(entity)
: statId,
fill: true,
stepped: false,
order: ORDER[type] + idx,
borderColor:
idx > 0
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(color.border)), idx)))
@ -394,7 +398,7 @@ export class HuiEnergySummaryGraphCard
lab2rgb(labDarken(rgb2lab(hex2rgb(color.background)), idx))
)
: color.background,
stack: negative ? "negative" : "positive",
stack: "stack",
data: [],
});
@ -402,6 +406,7 @@ export class HuiEnergySummaryGraphCard
for (const key of uniqueKeys) {
const value = key in source ? Math.round(source[key] * 100) / 100 : 0;
const date = new Date(key);
// @ts-expect-error
data[0].data.push({
x: date.getTime(),
y: value && negative ? -1 * value : value,

View File

@ -1,322 +0,0 @@
import { mdiHome, mdiLeaf, mdiSolarPower, mdiTransmissionTower } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { subscribeOne } from "../../../common/util/subscribe-one";
import "../../../components/ha-svg-icon";
import { getConfigEntries } from "../../../data/config_entries";
import { energySourcesByType } from "../../../data/energy";
import { subscribeEntityRegistry } from "../../../data/entity_registry";
import {
calculateStatisticsSumGrowth,
fetchStatistics,
Statistics,
} from "../../../data/history";
import { HomeAssistant } from "../../../types";
import { LovelaceCard } from "../types";
import { EnergySummaryCardConfig } from "./types";
import "../../../components/ha-card";
import { round } from "../../../common/number/round";
@customElement("hui-energy-usage-card")
class HuiEnergyUsageCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: EnergySummaryCardConfig;
@state() private _stats?: Statistics;
@state() private _co2SignalEntity?: string;
private _fetching = false;
public setConfig(config: EnergySummaryCardConfig): void {
this._config = config;
}
public getCardSize(): Promise<number> | number {
return 3;
}
public willUpdate(changedProps) {
super.willUpdate(changedProps);
if (!this._fetching && !this._stats) {
this._fetching = true;
Promise.all([this._getStatistics(), this._fetchCO2SignalEntity()]).then(
() => {
this._fetching = false;
}
);
}
}
protected render() {
if (!this._config) {
return html``;
}
if (!this._stats) {
return html`Loading…`;
}
const prefs = this._config!.prefs;
const types = energySourcesByType(prefs);
// The strategy only includes this card if we have a grid.
const hasConsumption = true;
const hasSolarProduction = types.solar !== undefined;
const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0;
const totalGridConsumption =
calculateStatisticsSumGrowth(
this._stats,
types.grid![0].flow_from.map((flow) => flow.stat_energy_from)
) ?? 0;
let totalSolarProduction: number | null = null;
if (hasSolarProduction) {
totalSolarProduction = calculateStatisticsSumGrowth(
this._stats,
types.solar!.map((source) => source.stat_energy_from)
);
}
let productionReturnedToGrid: number | null = null;
if (hasReturnToGrid) {
productionReturnedToGrid = calculateStatisticsSumGrowth(
this._stats,
types.grid![0].flow_to.map((flow) => flow.stat_energy_to)
);
}
// total consumption = consumption_from_grid + solar_production - return_to_grid
let co2percentage: number | undefined;
if (this._co2SignalEntity) {
const co2State = this.hass.states[this._co2SignalEntity];
if (co2State) {
co2percentage = Number(co2State.state);
if (isNaN(co2percentage)) {
co2percentage = undefined;
}
}
}
// We are calculating low carbon consumption based on what we got from the grid
// minus what we gave back because what we gave back is low carbon
const relativeGridFlow =
totalGridConsumption - (productionReturnedToGrid || 0);
let lowCarbonConsumption: number | undefined;
if (co2percentage !== undefined) {
if (relativeGridFlow > 0) {
lowCarbonConsumption = round(relativeGridFlow * (co2percentage / 100));
} else {
lowCarbonConsumption = 0;
}
}
const totalConsumption =
totalGridConsumption +
(totalSolarProduction || 0) -
(productionReturnedToGrid || 0);
const gridPctLowCarbon =
co2percentage === undefined ? 0 : co2percentage / 100;
const gridPctHighCarbon = 1 - gridPctLowCarbon;
const homePctSolar =
((totalSolarProduction || 0) - (productionReturnedToGrid || 0)) /
totalConsumption;
// When we know the ratio solar-grid, we can adjust the low/high carbon
// percentages to reflect that.
const homePctGridLowCarbon = gridPctLowCarbon * (1 - homePctSolar);
const homePctGridHighCarbon = gridPctHighCarbon * (1 - homePctSolar);
return html`
<ha-card header="Usage">
<div class="card-content">
<div class="row">
${co2percentage === undefined
? ""
: html`
<div class="circle-container">
<span class="label">Low-carbon</span>
<div class="circle low-carbon">
<ha-svg-icon .path="${mdiLeaf}"></ha-svg-icon>
${co2percentage}% / ${round(lowCarbonConsumption!)} kWh
</div>
</div>
`}
<div class="circle-container">
<span class="label">Solar</span>
<div class="circle solar">
<ha-svg-icon .path="${mdiSolarPower}"></ha-svg-icon>
${round(totalSolarProduction || 0)} kWh
</div>
</div>
</div>
<div class="row">
<div class="circle-container">
<div class="circle grid">
<ha-svg-icon .path="${mdiTransmissionTower}"></ha-svg-icon>
${round(totalGridConsumption - (productionReturnedToGrid || 0))}
kWh
<ul>
<li>
Grid high carbon: ${round(gridPctHighCarbon * 100, 1)}%
</li>
<li>Grid low carbon: ${round(gridPctLowCarbon * 100, 1)}%</li>
</ul>
</div>
<span class="label">Grid</span>
</div>
<div class="circle-container home">
<div class="circle home">
<ha-svg-icon .path="${mdiHome}"></ha-svg-icon>
${round(totalConsumption)} kWh
<ul>
<li>
Grid high carbon: ${round(homePctGridHighCarbon * 100)}%
</li>
<li>
Grid low carbon: ${round(homePctGridLowCarbon * 100)}%
</li>
<li>Solar: ${round(homePctSolar * 100)}%</li>
</ul>
</div>
<span class="label">Home</span>
</div>
</div>
</div>
</ha-card>
`;
}
private async _fetchCO2SignalEntity() {
const [configEntries, entityRegistryEntries] = await Promise.all([
getConfigEntries(this.hass),
subscribeOne(this.hass.connection, subscribeEntityRegistry),
]);
const co2ConfigEntry = configEntries.find(
(entry) => entry.domain === "co2signal"
);
if (!co2ConfigEntry) {
return;
}
for (const entry of entityRegistryEntries) {
if (entry.config_entry_id !== co2ConfigEntry.entry_id) {
continue;
}
// The integration offers 2 entities. We want the % one.
const co2State = this.hass.states[entry.entity_id];
if (!co2State || co2State.attributes.unit_of_measurement !== "%") {
continue;
}
this._co2SignalEntity = co2State.entity_id;
break;
}
}
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[] = [];
const prefs = this._config!.prefs;
for (const source of prefs.energy_sources) {
if (source.type === "solar") {
statistics.push(source.stat_energy_from);
continue;
}
// grid source
for (const flowFrom of source.flow_from) {
statistics.push(flowFrom.stat_energy_from);
}
for (const flowTo of source.flow_to) {
statistics.push(flowTo.stat_energy_to);
}
}
this._stats = await fetchStatistics(
this.hass!,
startDate,
undefined,
statistics
);
}
static styles = css`
:host {
--mdc-icon-size: 26px;
}
.row {
display: flex;
margin-bottom: 30px;
}
.row:last-child {
margin-bottom: 0;
}
.circle-container {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 40px;
}
.circle {
width: 80px;
height: 80px;
border-radius: 50%;
border: 2px solid;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
font-size: 12px;
}
.label {
color: var(--secondary-text-color);
font-size: 12px;
}
.circle-container:last-child {
margin-right: 0;
}
.circle ul {
display: none;
}
.low-carbon {
border-color: #0da035;
}
.low-carbon ha-svg-icon {
color: #0da035;
}
.solar {
border-color: #ff9800;
}
.grid {
border-color: #134763;
}
.circle-container.home {
margin-left: 120px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hui-energy-usage-card": HuiEnergyUsageCard;
}
}

View File

@ -92,31 +92,42 @@ export interface ButtonCardConfig extends LovelaceCardConfig {
export interface EnergySummaryCardConfig extends LovelaceCardConfig {
type: "energy-summary";
title?: string;
prefs: EnergyPreferences;
}
export interface EnergyDistributionCardConfig extends LovelaceCardConfig {
type: "energy-distribution";
title?: string;
prefs: EnergyPreferences;
}
export interface EnergySummaryGraphCardConfig extends LovelaceCardConfig {
type: "energy-summary-graph";
title?: string;
prefs: EnergyPreferences;
}
export interface EnergySolarGraphCardConfig extends LovelaceCardConfig {
type: "energy-solar-graph";
title?: string;
prefs: EnergyPreferences;
}
export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig {
type: "energy-devices-graph";
title?: string;
prefs: EnergyPreferences;
}
export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig {
type: "energy-solar-consumed-gauge";
title?: string;
prefs: EnergyPreferences;
}
export interface EnergyCarbonGaugeCardConfig extends LovelaceCardConfig {
type: "energy-carbon-consumed-gauge";
title?: string;
prefs: EnergyPreferences;
}

View File

@ -35,18 +35,21 @@ 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": () => import("../cards/hui-energy-summary-card"),
"energy-summary": () => import("../cards/energy/hui-energy-summary-card"),
"energy-summary-graph": () =>
import("../cards/hui-energy-summary-graph-card"),
"energy-solar-graph": () => import("../cards/hui-energy-solar-graph-card"),
import("../cards/energy/hui-energy-summary-graph-card"),
"energy-solar-graph": () =>
import("../cards/energy/hui-energy-solar-graph-card"),
"energy-devices-graph": () =>
import("../cards/hui-energy-devices-graph-card"),
"energy-costs-table": () => import("../cards/hui-energy-costs-table-card"),
"energy-usage": () => import("../cards/hui-energy-usage-card"),
import("../cards/energy/hui-energy-devices-graph-card"),
"energy-costs-table": () =>
import("../cards/energy/hui-energy-costs-table-card"),
"energy-distribution": () =>
import("../cards/energy/hui-energy-distribution-card"),
"energy-solar-consumed-gauge": () =>
import("../cards/hui-energy-solar-consumed-gauge-card"),
import("../cards/energy/hui-energy-solar-consumed-gauge-card"),
"energy-carbon-consumed-gauge": () =>
import("../cards/hui-energy-carbon-consumed-gauge-card"),
import("../cards/energy/hui-energy-carbon-consumed-gauge-card"),
grid: () => import("../cards/hui-grid-card"),
starting: () => import("../cards/hui-starting-card"),
"entity-filter": () => import("../cards/hui-entity-filter-card"),