diff --git a/demo/src/stubs/energy.ts b/demo/src/stubs/energy.ts index 80af3d1ea1..b7e0c3f280 100644 --- a/demo/src/stubs/energy.ts +++ b/demo/src/stubs/energy.ts @@ -49,6 +49,14 @@ export const mockEnergy = (hass: MockHomeAssistant) => { stat_energy_from: "sensor.battery_output", stat_energy_to: "sensor.battery_input", }, + { + type: "gas", + stat_energy_from: "sensor.energy_gas", + stat_cost: "sensor.energy_gas_cost", + entity_energy_from: "sensor.energy_gas", + entity_energy_price: null, + number_energy_price: null, + }, ], device_consumption: [ { diff --git a/demo/src/stubs/entities.ts b/demo/src/stubs/entities.ts index df7075e161..132515a014 100644 --- a/demo/src/stubs/entities.ts +++ b/demo/src/stubs/entities.ts @@ -104,6 +104,23 @@ export const energyEntities = () => unit_of_measurement: "EUR", }, }, + "sensor.energy_gas_cost": { + entity_id: "sensor.energy_gas_cost", + state: "2", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + unit_of_measurement: "EUR", + }, + }, + "sensor.energy_gas": { + entity_id: "sensor.energy_gas", + state: "4", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + friendly_name: "Gas", + unit_of_measurement: "m³", + }, + }, "sensor.energy_car": { entity_id: "sensor.energy_car", state: "4", diff --git a/src/data/energy.ts b/src/data/energy.ts index 055e61cadb..8e801ced29 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -53,6 +53,14 @@ export const emptyBatteryEnergyPreference = stat_energy_from: "", stat_energy_to: "", }); +export const emptyGasEnergyPreference = (): GasSourceTypeEnergyPreference => ({ + type: "gas", + stat_energy_from: "", + stat_cost: null, + entity_energy_from: null, + entity_energy_price: null, + number_energy_price: null, +}); export interface DeviceConsumptionEnergyPreference { // This is an ever increasing value @@ -106,11 +114,26 @@ export interface BatterySourceTypeEnergyPreference { stat_energy_from: string; stat_energy_to: string; } +export interface GasSourceTypeEnergyPreference { + type: "gas"; + + // kWh meter + stat_energy_from: string; + + // $ meter + stat_cost: string | null; + + // Can be used to generate costs if stat_cost omitted + entity_energy_from: string | null; + entity_energy_price: string | null; + number_energy_price: number | null; +} type EnergySource = | SolarSourceTypeEnergyPreference | GridSourceTypeEnergyPreference - | BatterySourceTypeEnergyPreference; + | BatterySourceTypeEnergyPreference + | GasSourceTypeEnergyPreference; export interface EnergyPreferences { energy_sources: EnergySource[]; @@ -147,6 +170,7 @@ interface EnergySourceByType { grid?: GridSourceTypeEnergyPreference[]; solar?: SolarSourceTypeEnergyPreference[]; battery?: BatterySourceTypeEnergyPreference[]; + gas?: GasSourceTypeEnergyPreference[]; } export const energySourcesByType = (prefs: EnergyPreferences) => { @@ -218,6 +242,18 @@ const getEnergyData = async ( continue; } + if (source.type === "gas") { + statIDs.push(source.stat_energy_from); + if (source.stat_cost) { + statIDs.push(source.stat_cost); + } + const costStatId = info.cost_sensors[source.stat_energy_from]; + if (costStatId) { + statIDs.push(costStatId); + } + continue; + } + if (source.type === "battery") { statIDs.push(source.stat_energy_from); statIDs.push(source.stat_energy_to); diff --git a/src/panels/config/energy/components/ha-energy-device-settings.ts b/src/panels/config/energy/components/ha-energy-device-settings.ts index b9c63ce6c6..561973a0e0 100644 --- a/src/panels/config/energy/components/ha-energy-device-settings.ts +++ b/src/panels/config/energy/components/ha-energy-device-settings.ts @@ -5,9 +5,7 @@ import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeStateName } from "../../../../common/entity/compute_state_name"; import { stateIcon } from "../../../../common/entity/state_icon"; -import "../../../../components/entity/ha-statistic-picker"; import "../../../../components/ha-card"; -import "../../../../components/ha-settings-row"; import { DeviceConsumptionEnergyPreference, EnergyPreferences, diff --git a/src/panels/config/energy/components/ha-energy-gas-settings.ts b/src/panels/config/energy/components/ha-energy-gas-settings.ts new file mode 100644 index 0000000000..00fc47e1df --- /dev/null +++ b/src/panels/config/energy/components/ha-energy-gas-settings.ts @@ -0,0 +1,151 @@ +import "@material/mwc-button/mwc-button"; +import { mdiDelete, mdiFire, mdiPencil } from "@mdi/js"; +import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { computeStateName } from "../../../../common/entity/compute_state_name"; +import "../../../../components/ha-card"; +import { + EnergyPreferences, + energySourcesByType, + saveEnergyPreferences, + GasSourceTypeEnergyPreference, +} from "../../../../data/energy"; +import { + showConfirmationDialog, + showAlertDialog, +} from "../../../../dialogs/generic/show-dialog-box"; +import { haStyle } from "../../../../resources/styles"; +import { HomeAssistant } from "../../../../types"; +import { documentationUrl } from "../../../../util/documentation-url"; +import { showEnergySettingsGasDialog } from "../dialogs/show-dialogs-energy"; +import { energyCardStyles } from "./styles"; + +@customElement("ha-energy-gas-settings") +export class EnergyGasSettings extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) + public preferences!: EnergyPreferences; + + protected render(): TemplateResult { + const types = energySourcesByType(this.preferences); + + const gasSources = types.gas || []; + + return html` + +

+ + ${this.hass.localize("ui.panel.config.energy.gas.title")} +

+ +
+

+ ${this.hass.localize("ui.panel.config.energy.gas.sub")} + ${this.hass.localize("ui.panel.config.energy.gas.learn_more")} +

+

Gas consumption

+ ${gasSources.map((source) => { + const entityState = this.hass.states[source.stat_energy_from]; + return html` +
+ ${entityState?.attributes.icon + ? html`` + : html``} + ${entityState + ? computeStateName(entityState) + : source.stat_energy_from} + + + + + + +
+ `; + })} +
+ + Add gas source +
+
+
+ `; + } + + private _addSource() { + showEnergySettingsGasDialog(this, { + saveCallback: async (source) => { + await this._savePreferences({ + ...this.preferences, + energy_sources: this.preferences.energy_sources.concat(source), + }); + }, + }); + } + + private _editSource(ev) { + const origSource: GasSourceTypeEnergyPreference = + ev.currentTarget.closest(".row").source; + showEnergySettingsGasDialog(this, { + source: { ...origSource }, + saveCallback: async (newSource) => { + await this._savePreferences({ + ...this.preferences, + energy_sources: this.preferences.energy_sources.map((src) => + src === origSource ? newSource : src + ), + }); + }, + }); + } + + private async _deleteSource(ev) { + const sourceToDelete: GasSourceTypeEnergyPreference = + ev.currentTarget.closest(".row").source; + + if ( + !(await showConfirmationDialog(this, { + title: "Are you sure you want to delete this source?", + })) + ) { + return; + } + + try { + await this._savePreferences({ + ...this.preferences, + energy_sources: this.preferences.energy_sources.filter( + (source) => source !== sourceToDelete + ), + }); + } catch (err) { + showAlertDialog(this, { title: `Failed to save config: ${err.message}` }); + } + } + + private async _savePreferences(preferences: EnergyPreferences) { + const result = await saveEnergyPreferences(this.hass, preferences); + fireEvent(this, "value-changed", { value: result }); + } + + static get styles(): CSSResultGroup { + return [haStyle, energyCardStyles]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-energy-gas-settings": EnergyGasSettings; + } +} diff --git a/src/panels/config/energy/components/ha-energy-grid-settings.ts b/src/panels/config/energy/components/ha-energy-grid-settings.ts index cfdf31463b..5bf861f9d4 100644 --- a/src/panels/config/energy/components/ha-energy-grid-settings.ts +++ b/src/panels/config/energy/components/ha-energy-grid-settings.ts @@ -10,9 +10,7 @@ import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeStateName } from "../../../../common/entity/compute_state_name"; -import "../../../../components/entity/ha-statistic-picker"; import "../../../../components/ha-card"; -import "../../../../components/ha-settings-row"; import { ConfigEntry, deleteConfigEntry, diff --git a/src/panels/config/energy/components/ha-energy-solar-settings.ts b/src/panels/config/energy/components/ha-energy-solar-settings.ts index 0093c47503..109c61d36e 100644 --- a/src/panels/config/energy/components/ha-energy-solar-settings.ts +++ b/src/panels/config/energy/components/ha-energy-solar-settings.ts @@ -4,9 +4,7 @@ import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeStateName } from "../../../../common/entity/compute_state_name"; -import "../../../../components/entity/ha-statistic-picker"; import "../../../../components/ha-card"; -import "../../../../components/ha-settings-row"; import { EnergyPreferences, energySourcesByType, diff --git a/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts new file mode 100644 index 0000000000..e711615c41 --- /dev/null +++ b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts @@ -0,0 +1,277 @@ +import { mdiFire } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-dialog"; +import { + emptyGasEnergyPreference, + GasSourceTypeEnergyPreference, +} from "../../../../data/energy"; +import { HassDialog } from "../../../../dialogs/make-dialog-manager"; +import { haStyle, haStyleDialog } from "../../../../resources/styles"; +import { HomeAssistant } from "../../../../types"; +import { EnergySettingsGasDialogParams } from "./show-dialogs-energy"; +import "@material/mwc-button/mwc-button"; +import "../../../../components/entity/ha-statistic-picker"; +import "../../../../components/entity/ha-entity-picker"; +import "../../../../components/ha-radio"; +import "../../../../components/ha-formfield"; +import type { HaRadio } from "../../../../components/ha-radio"; + +const energyUnits = ["m³"]; + +@customElement("dialog-energy-gas-settings") +export class DialogEnergyGasSettings + extends LitElement + implements HassDialog +{ + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: EnergySettingsGasDialogParams; + + @state() private _source?: GasSourceTypeEnergyPreference; + + @state() private _costs?: "no-costs" | "number" | "entity" | "statistic"; + + @state() private _error?: string; + + public async showDialog( + params: EnergySettingsGasDialogParams + ): Promise { + this._params = params; + this._source = params.source + ? { ...params.source } + : (this._source = emptyGasEnergyPreference()); + this._costs = this._source.entity_energy_price + ? "entity" + : this._source.number_energy_price + ? "number" + : this._source.stat_cost + ? "statistic" + : "no-costs"; + } + + public closeDialog(): void { + this._params = undefined; + this._source = undefined; + this._error = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._params || !this._source) { + return html``; + } + + return html` + + Configure Gas consumption`} + @closed=${this.closeDialog} + > + ${this._error ? html`

${this._error}

` : ""} + + + +

+ ${this.hass.localize(`ui.panel.config.energy.gas.dialog.cost_para`)} +

+ + + + + + + + ${this._costs === "statistic" + ? html`` + : ""} + + + + ${this._costs === "entity" + ? html`` + : ""} + + + + ${this._costs === "number" + ? html` + ${this.hass.localize( + `ui.panel.config.energy.gas.dialog.cost_number_suffix`, + { currency: this.hass.config.currency } + )} + ` + : ""} + + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize("ui.common.save")} + +
+ `; + } + + private _handleCostChanged(ev: CustomEvent) { + const input = ev.currentTarget as HaRadio; + this._costs = input.value as any; + } + + private _numberPriceChanged(ev: CustomEvent) { + this._source = { + ...this._source!, + number_energy_price: Number(ev.detail.value), + entity_energy_price: null, + stat_cost: null, + }; + } + + private _priceStatChanged(ev: CustomEvent) { + this._source = { + ...this._source!, + entity_energy_price: null, + number_energy_price: null, + stat_cost: ev.detail.value, + }; + } + + private _priceEntityChanged(ev: CustomEvent) { + this._source = { + ...this._source!, + entity_energy_price: ev.detail.value, + number_energy_price: null, + stat_cost: null, + }; + } + + private _statisticChanged(ev: CustomEvent<{ value: string }>) { + this._source = { + ...this._source!, + stat_energy_from: ev.detail.value, + entity_energy_from: ev.detail.value, + }; + } + + private async _save() { + try { + if (this._costs === "no-costs") { + this._source!.entity_energy_price = null; + this._source!.number_energy_price = null; + this._source!.stat_cost = null; + } + await this._params!.saveCallback(this._source!); + this.closeDialog(); + } catch (e) { + this._error = e.message; + } + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleDialog, + css` + ha-dialog { + --mdc-dialog-max-width: 430px; + } + ha-formfield { + display: block; + } + .price-options { + display: block; + padding-left: 52px; + margin-top: -16px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-energy-gas-settings": DialogEnergyGasSettings; + } +} diff --git a/src/panels/config/energy/dialogs/show-dialogs-energy.ts b/src/panels/config/energy/dialogs/show-dialogs-energy.ts index 718a09791a..296533da8c 100644 --- a/src/panels/config/energy/dialogs/show-dialogs-energy.ts +++ b/src/panels/config/energy/dialogs/show-dialogs-energy.ts @@ -4,6 +4,7 @@ import { DeviceConsumptionEnergyPreference, FlowFromGridSourceEnergyPreference, FlowToGridSourceEnergyPreference, + GasSourceTypeEnergyPreference, SolarSourceTypeEnergyPreference, } from "../../../../data/energy"; @@ -39,6 +40,11 @@ export interface EnergySettingsBatteryDialogParams { saveCallback: (source: BatterySourceTypeEnergyPreference) => Promise; } +export interface EnergySettingsGasDialogParams { + source?: GasSourceTypeEnergyPreference; + saveCallback: (source: GasSourceTypeEnergyPreference) => Promise; +} + export interface EnergySettingsDeviceDialogParams { saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise; } @@ -76,6 +82,17 @@ export const showEnergySettingsSolarDialog = ( }); }; +export const showEnergySettingsGasDialog = ( + element: HTMLElement, + dialogParams: EnergySettingsGasDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-energy-gas-settings", + dialogImport: () => import("./dialog-energy-gas-settings"), + dialogParams: dialogParams, + }); +}; + export const showEnergySettingsGridFlowFromDialog = ( element: HTMLElement, dialogParams: EnergySettingsGridFlowFromDialogParams diff --git a/src/panels/config/energy/ha-config-energy.ts b/src/panels/config/energy/ha-config-energy.ts index 1520ab3848..96a98e1251 100644 --- a/src/panels/config/energy/ha-config-energy.ts +++ b/src/panels/config/energy/ha-config-energy.ts @@ -11,6 +11,7 @@ import "./components/ha-energy-device-settings"; import "./components/ha-energy-grid-settings"; import "./components/ha-energy-solar-settings"; import "./components/ha-energy-battery-settings"; +import "./components/ha-energy-gas-settings"; const INITIAL_CONFIG: EnergyPreferences = { energy_sources: [], @@ -87,6 +88,11 @@ class HaConfigEnergy extends LitElement { .preferences=${this._preferences!} @value-changed=${this._prefsChanged} > + ` + : this._step === 3 + ? html`` : html`${this.hass.localize("ui.panel.energy.setup.back")}` : html`
`} - ${this._step < 3 + ${this._step < 4 ? html`${this.hass.localize("ui.panel.energy.setup.next")}` @@ -95,7 +102,7 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard { } private _next() { - if (this._step === 2) { + if (this._step === 4) { return; } this._step++; diff --git a/src/panels/energy/strategies/energy-strategy.ts b/src/panels/energy/strategies/energy-strategy.ts index 49cbd113d0..60485da740 100644 --- a/src/panels/energy/strategies/energy-strategy.ts +++ b/src/panels/energy/strategies/energy-strategy.ts @@ -50,6 +50,7 @@ export class EnergyStrategy { const hasSolar = prefs.energy_sources.some( (source) => source.type === "solar" ); + const hasGas = prefs.energy_sources.some((source) => source.type === "gas"); if (info.narrow) { view.cards!.push({ @@ -77,6 +78,15 @@ export class EnergyStrategy { }); } + // Only include if we have a gas source. + if (hasGas) { + view.cards!.push({ + title: "Gas consumption", + type: "energy-gas-graph", + collection_key: "energy_dashboard", + }); + } + // Only include if we have a grid. if (hasGrid) { view.cards!.push({ diff --git a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts index fedfe067cf..6990690623 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -4,6 +4,7 @@ import { mdiArrowRight, mdiArrowUp, mdiBatteryHigh, + mdiFire, mdiHome, mdiLeaf, mdiSolarPower, @@ -79,6 +80,7 @@ class HuiEnergyDistrubutionCard const hasSolarProduction = types.solar !== undefined; const hasBattery = types.battery !== undefined; + const hasGas = types.gas !== undefined; const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0; const totalFromGrid = @@ -87,6 +89,15 @@ class HuiEnergyDistrubutionCard types.grid![0].flow_from.map((flow) => flow.stat_energy_from) ) ?? 0; + let gasUsage: number | null = null; + if (hasGas) { + gasUsage = + calculateStatisticsSumGrowth( + this._data.stats, + types.gas!.map((source) => source.stat_energy_from) + ) ?? 0; + } + let totalSolarProduction: number | null = null; if (hasSolarProduction) { @@ -250,7 +261,7 @@ class HuiEnergyDistrubutionCard return html`
- ${lowCarbonEnergy !== undefined || hasSolarProduction + ${lowCarbonEnergy !== undefined || hasSolarProduction || hasGas ? html`
${lowCarbonEnergy === undefined ? html`
` @@ -287,8 +298,39 @@ class HuiEnergyDistrubutionCard kWh
` + : hasGas + ? html`
` : ""} -
+ ${hasGas + ? html`
+ Gas +
+ + ${formatNumber(gasUsage || 0, this.hass.locale, { + maximumFractionDigits: 1, + })} + m³ +
+ + + ${gasUsage + ? svg` + + + + ` + : ""} + +
` + : html`
`} ` : ""}
@@ -667,6 +709,10 @@ class HuiEnergyDistrubutionCard margin-right: 4px; } .circle-container.solar { + margin: 0 4px; + height: 130px; + } + .circle-container.gas { margin-left: 4px; height: 130px; } @@ -717,6 +763,17 @@ class HuiEnergyDistrubutionCard width: 100%; height: 100%; } + .gas path, + .gas circle { + stroke: var(--energy-gas-color); + } + circle.gas { + stroke-width: 4; + fill: var(--energy-gas-color); + } + .gas .circle { + border-color: var(--energy-gas-color); + } .low-carbon line { stroke: var(--energy-non-fossil-color); } diff --git a/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts new file mode 100644 index 0000000000..89ed6d3a9e --- /dev/null +++ b/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts @@ -0,0 +1,355 @@ +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import memoizeOne from "memoize-one"; +import { classMap } from "lit/directives/class-map"; +import "../../../../components/ha-card"; +import { + ChartData, + ChartDataset, + ChartOptions, + ScatterDataPoint, +} from "chart.js"; +import { + addHours, + differenceInDays, + endOfToday, + isToday, + startOfToday, +} from "date-fns"; +import { HomeAssistant } from "../../../../types"; +import { LovelaceCard } from "../../types"; +import { EnergyGasGraphCardConfig } from "../types"; +import { + hex2rgb, + lab2rgb, + rgb2hex, + rgb2lab, +} from "../../../../common/color/convert-color"; +import { labDarken } from "../../../../common/color/lab"; +import { + EnergyData, + getEnergyDataCollection, + GasSourceTypeEnergyPreference, +} from "../../../../data/energy"; +import { computeStateName } from "../../../../common/entity/compute_state_name"; +import "../../../../components/chart/ha-chart-base"; +import { + formatNumber, + numberFormatToLocale, +} from "../../../../common/string/format_number"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; +import { FrontendLocaleData } from "../../../../data/translation"; +import { + reduceSumStatisticsByMonth, + reduceSumStatisticsByDay, +} from "../../../../data/history"; + +@customElement("hui-energy-gas-graph-card") +export class HuiEnergyGasGraphCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _config?: EnergyGasGraphCardConfig; + + @state() private _chartData: ChartData = { + datasets: [], + }; + + @state() private _start = startOfToday(); + + @state() private _end = endOfToday(); + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass, { + key: this._config?.collection_key, + }).subscribe((data) => this._getStatistics(data)), + ]; + } + + public getCardSize(): Promise | number { + return 3; + } + + public setConfig(config: EnergyGasGraphCardConfig): void { + this._config = config; + } + + protected render(): TemplateResult { + if (!this.hass || !this._config) { + return html``; + } + + return html` + + ${this._config.title + ? html`

${this._config.title}

` + : ""} +
+ + ${!this._chartData.datasets.length + ? html`
+ ${isToday(this._start) + ? "There is no data to show. It can take up to 2 hours for new data to arrive after you configure your energy dashboard." + : "There is no data for this period."} +
` + : ""} +
+
+ `; + } + + private _createOptions = memoizeOne( + (start: Date, end: Date, locale: FrontendLocaleData): ChartOptions => { + const dayDifference = differenceInDays(end, start); + return { + parsing: false, + animation: false, + scales: { + x: { + type: "time", + suggestedMin: (dayDifference > 2 + ? addHours(start, -11) + : start + ).getTime(), + suggestedMax: (dayDifference > 2 + ? addHours(end, -11) + : end + ).getTime(), + adapters: { + date: { + locale: locale, + }, + }, + ticks: { + maxRotation: 0, + sampleSize: 5, + autoSkipPadding: 20, + major: { + enabled: true, + }, + font: (context) => + context.tick && context.tick.major + ? ({ weight: "bold" } as any) + : {}, + }, + time: { + tooltipFormat: + dayDifference > 35 + ? "monthyear" + : dayDifference > 7 + ? "date" + : dayDifference > 2 + ? "weekday" + : dayDifference > 0 + ? "datetime" + : "hour", + minUnit: + dayDifference > 35 + ? "month" + : dayDifference > 2 + ? "day" + : "hour", + }, + offset: true, + }, + y: { + type: "linear", + title: { + display: true, + text: "m³", + }, + ticks: { + beginAtZero: true, + }, + }, + }, + plugins: { + tooltip: { + mode: "nearest", + callbacks: { + label: (context) => + `${context.dataset.label}: ${formatNumber( + context.parsed.y, + locale + )} m³`, + }, + }, + filler: { + propagate: false, + }, + legend: { + display: false, + labels: { + usePointStyle: true, + }, + }, + }, + hover: { + mode: "nearest", + }, + elements: { + bar: { borderWidth: 1.5, borderRadius: 4 }, + point: { + hitRadius: 5, + }, + }, + // @ts-expect-error + locale: numberFormatToLocale(locale), + }; + } + ); + + private async _getStatistics(energyData: EnergyData): Promise { + const gasSources: GasSourceTypeEnergyPreference[] = + energyData.prefs.energy_sources.filter( + (source) => source.type === "gas" + ) as GasSourceTypeEnergyPreference[]; + + const statisticsData = Object.values(energyData.stats); + const datasets: ChartDataset<"bar">[] = []; + let endTime: Date; + + endTime = new Date( + Math.max( + ...statisticsData.map((stats) => + stats.length ? new Date(stats[stats.length - 1].start).getTime() : 0 + ) + ) + ); + + if (!endTime || endTime > new Date()) { + endTime = new Date(); + } + + const computedStyles = getComputedStyle(this); + const gasColor = computedStyles + .getPropertyValue("--energy-gas-color") + .trim(); + + const dayDifference = differenceInDays( + energyData.end || new Date(), + energyData.start + ); + + gasSources.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(gasColor)), idx))) + : gasColor; + + let prevValue: number | null = null; + let prevStart: string | null = null; + + const gasConsumptionData: ScatterDataPoint[] = []; + + // Process gas consumption data. + if (source.stat_energy_from in energyData.stats) { + const stats = + dayDifference > 35 + ? reduceSumStatisticsByMonth( + energyData.stats[source.stat_energy_from] + ) + : dayDifference > 2 + ? reduceSumStatisticsByDay( + energyData.stats[source.stat_energy_from] + ) + : energyData.stats[source.stat_energy_from]; + + for (const point of stats) { + if (point.sum === null) { + continue; + } + if (prevValue === null) { + prevValue = point.sum; + continue; + } + if (prevStart === point.start) { + continue; + } + const value = point.sum - prevValue; + const date = new Date(point.start); + gasConsumptionData.push({ + x: date.getTime(), + y: value, + }); + prevStart = point.start; + prevValue = point.sum; + } + } + + if (gasConsumptionData.length) { + data.push({ + label: entity ? computeStateName(entity) : source.stat_energy_from, + borderColor, + backgroundColor: borderColor + "7F", + data: gasConsumptionData, + }); + } + + // Concat two arrays + Array.prototype.push.apply(datasets, data); + }); + + this._start = energyData.start; + this._end = energyData.end || endOfToday(); + + this._chartData = { + datasets, + }; + } + + static get styles(): CSSResultGroup { + return css` + ha-card { + height: 100%; + } + .card-header { + padding-bottom: 0; + } + .content { + padding: 16px; + } + .has-header { + padding-top: 0; + } + .no-data { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + padding: 20%; + margin-left: 32px; + box-sizing: border-box; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-energy-gas-graph-card": HuiEnergyGasGraphCard; + } +} diff --git a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts index 79e7699a89..46b4adf9c8 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts @@ -72,9 +72,11 @@ export class HuiEnergySourcesTableCard } let totalGrid = 0; + let totalGridCost = 0; let totalSolar = 0; let totalBattery = 0; - let totalCost = 0; + let totalGas = 0; + let totalGasCost = 0; const types = energySourcesByType(this._data.prefs); @@ -94,6 +96,9 @@ export class HuiEnergySourcesTableCard const consumptionColor = computedStyles .getPropertyValue("--energy-grid-consumption-color") .trim(); + const gasColor = computedStyles + .getPropertyValue("--energy-gas-color") + .trim(); const showCosts = types.grid?.[0].flow_from.some( @@ -105,6 +110,10 @@ export class HuiEnergySourcesTableCard flow.stat_compensation || flow.entity_energy_price || flow.number_energy_price + ) || + types.gas?.some( + (flow) => + flow.stat_cost || flow.entity_energy_price || flow.number_energy_price ); return html` @@ -113,7 +122,7 @@ export class HuiEnergySourcesTableCard : ""}
- +
@@ -307,7 +316,7 @@ export class HuiEnergySourcesTableCard ) || 0 : null; if (cost !== null) { - totalCost += cost; + totalGridCost += cost; } const color = idx > 0 @@ -367,7 +376,7 @@ export class HuiEnergySourcesTableCard ) || 0) * -1 : null; if (cost !== null) { - totalCost += cost; + totalGridCost += cost; } const color = idx > 0 @@ -421,7 +430,7 @@ export class HuiEnergySourcesTableCard ? html`` : ""} + ${types.gas?.map((source, idx) => { + const entity = this.hass.states[source.stat_energy_from]; + const energy = + calculateStatisticSumGrowth( + this._data!.stats[source.stat_energy_from] + ) || 0; + totalGas += energy; + const cost_stat = + source.stat_cost || + this._data!.info.cost_sensors[source.stat_energy_from]; + const cost = cost_stat + ? calculateStatisticSumGrowth(this._data!.stats[cost_stat]) || + 0 + : null; + if (cost !== null) { + totalGasCost += cost; + } + const color = + idx > 0 + ? rgb2hex( + lab2rgb(labDarken(rgb2lab(hex2rgb(gasColor)), idx)) + ) + : gasColor; + return html` + + + + ${showCosts + ? html`` + : ""} + `; + })} + ${types.gas + ? html` + + + + ${showCosts + ? html`` + : ""} + ` + : ""} + ${totalGasCost && totalGridCost + ? html` + + + + + ` + : ""}
- ${formatNumber(totalCost, this.hass.locale, { + ${formatNumber(totalGridCost, this.hass.locale, { style: "currency", currency: this.hass.config.currency!, })} @@ -429,6 +438,105 @@ export class HuiEnergySourcesTableCard : ""}
+
+
+ ${entity + ? computeStateName(entity) + : source.stat_energy_from} + + ${formatNumber(energy, this.hass.locale)} m³ + + ${cost !== null + ? formatNumber(cost, this.hass.locale, { + style: "currency", + currency: this.hass.config.currency!, + }) + : ""} +
Gas total + ${formatNumber(totalGas, this.hass.locale)} m³ + + ${formatNumber(totalGasCost, this.hass.locale, { + style: "currency", + currency: this.hass.config.currency!, + })} +
+ Total costs + + ${formatNumber( + totalGasCost + totalGridCost, + this.hass.locale, + { + style: "currency", + currency: this.hass.config.currency!, + } + )} +
diff --git a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts index 94b4a388e5..a8fc4dc913 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts @@ -269,6 +269,10 @@ export class HuiEnergyUsageGraphCard continue; } + if (source.type !== "grid") { + continue; + } + // grid source for (const flowFrom of source.flow_from) { if (statistics.from_grid) { diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 285f7798f6..ffc25948da 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -113,6 +113,12 @@ export interface EnergySolarGraphCardConfig extends LovelaceCardConfig { collection_key?: string; } +export interface EnergyGasGraphCardConfig extends LovelaceCardConfig { + type: "energy-gas-graph"; + title?: string; + collection_key?: string; +} + export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig { type: "energy-devices-graph"; title?: string; diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index 151f8b3638..6c8c470e35 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -39,6 +39,7 @@ const LAZY_LOAD_TYPES = { import("../cards/energy/hui-energy-usage-graph-card"), "energy-solar-graph": () => import("../cards/energy/hui-energy-solar-graph-card"), + "energy-gas-graph": () => import("../cards/energy/hui-energy-gas-graph-card"), "energy-devices-graph": () => import("../cards/energy/hui-energy-devices-graph-card"), "energy-sources-table": () => diff --git a/src/resources/ha-style.ts b/src/resources/ha-style.ts index 1cf30e1054..f7b0acac80 100644 --- a/src/resources/ha-style.ts +++ b/src/resources/ha-style.ts @@ -90,6 +90,7 @@ documentContainer.innerHTML = ` --energy-non-fossil-color: #0f9d58; --energy-battery-out-color: #4db6ac; --energy-battery-in-color: #f06292; + --energy-gas-color: #8E021B; /* opacity for dark text on a light background */ --dark-divider-opacity: 0.12; diff --git a/src/translations/en.json b/src/translations/en.json index ff9df30afb..4dbc1ce1e0 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1051,6 +1051,25 @@ "sub": "If you have a battery system, you can configure it to monitor how much energy was stored and used from your battery.", "learn_more": "More information on how to get started." }, + "gas": { + "title": "Gas Consumption", + "sub": "Let Home Assistant monitor your gas usage.", + "learn_more": "More information on how to get started.", + "dialog": { + "header": "Configure gas consumption", + "paragraph": "Gas consumption is the volume of gas that flows from to your home.", + "energy_stat": "Consumed Energy (m³)", + "cost_para": "Select how Home Assistant should keep track of the costs of the consumed energy.", + "no_cost": "Do not track costs", + "cost_stat": "Use an entity tracking the total costs", + "cost_stat_input": "Total Costs Entity", + "cost_entity": "Use an entity with current price", + "cost_entity_input": "Entity with the current price", + "cost_number": "Use a static price", + "cost_number_input": "Price per m³", + "cost_number_suffix": "{currency}/m³" + } + }, "device_consumption": { "title": "Individual devices", "sub": "Tracking the energy usage of individual devices allows Home Assistant to break down your energy usage by device.",