diff --git a/src/components/entity/ha-statistic-picker.ts b/src/components/entity/ha-statistic-picker.ts index e437837dd816..e5fde55fc888 100644 --- a/src/components/entity/ha-statistic-picker.ts +++ b/src/components/entity/ha-statistic-picker.ts @@ -21,7 +21,6 @@ import "../ha-combo-box-item"; import "../ha-generic-picker"; import type { HaGenericPicker } from "../ha-generic-picker"; import "../ha-icon-button"; -import "../ha-input-helper-text"; import type { PickerComboBoxItem, PickerComboBoxSearchFn, @@ -477,6 +476,7 @@ export class HaStatisticPicker extends LitElement { .hideClearIcon=${this.hideClearIcon} .searchFn=${this._searchFn} .valueRenderer=${this._valueRenderer} + .helper=${this.helper} @value-changed=${this._valueChanged} > diff --git a/src/data/energy.ts b/src/data/energy.ts index a91c8f2e302a..480135cdee10 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -102,6 +102,7 @@ export type EnergySolarForecasts = Record; export interface DeviceConsumptionEnergyPreference { // This is an ever increasing value stat_consumption: string; + stat_rate?: string; name?: string; included_in_stat?: string; } @@ -130,11 +131,17 @@ export interface FlowToGridSourceEnergyPreference { number_energy_price: number | null; } +export interface GridPowerSourceEnergyPreference { + // W meter + stat_rate: string; +} + export interface GridSourceTypeEnergyPreference { type: "grid"; flow_from: FlowFromGridSourceEnergyPreference[]; flow_to: FlowToGridSourceEnergyPreference[]; + power?: GridPowerSourceEnergyPreference[]; cost_adjustment_day: number; } @@ -143,6 +150,7 @@ export interface SolarSourceTypeEnergyPreference { type: "solar"; stat_energy_from: string; + stat_rate?: string; config_entry_solar_forecast: string[] | null; } @@ -150,6 +158,7 @@ export interface BatterySourceTypeEnergyPreference { type: "battery"; stat_energy_from: string; stat_energy_to: string; + stat_rate?: string; } export interface GasSourceTypeEnergyPreference { type: "gas"; 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 766aa0858844..eb48e4089a45 100644 --- a/src/panels/config/energy/components/ha-energy-grid-settings.ts +++ b/src/panels/config/energy/components/ha-energy-grid-settings.ts @@ -26,6 +26,7 @@ import type { EnergySource, FlowFromGridSourceEnergyPreference, FlowToGridSourceEnergyPreference, + GridPowerSourceEnergyPreference, GridSourceTypeEnergyPreference, } from "../../../../data/energy"; import { @@ -47,6 +48,7 @@ import { documentationUrl } from "../../../../util/documentation-url"; import { showEnergySettingsGridFlowFromDialog, showEnergySettingsGridFlowToDialog, + showEnergySettingsGridPowerDialog, } from "../dialogs/show-dialogs-energy"; import "./ha-energy-validation-result"; import { energyCardStyles } from "./styles"; @@ -226,6 +228,58 @@ export class EnergyGridSettings extends LitElement { > +

+ ${this.hass.localize("ui.panel.config.energy.grid.grid_power")} +

+ ${gridSource.power?.map((power) => { + const entityState = this.hass.states[power.stat_rate]; + return html` +
+ ${entityState?.attributes.icon + ? html`` + : html``} + ${getStatisticLabel( + this.hass, + power.stat_rate, + this.statsMetadata?.[power.stat_rate] + )} + + +
+ `; + })} +
+ + + ${this.hass.localize( + "ui.panel.config.energy.grid.add_power" + )} +
+

${this.hass.localize( "ui.panel.config.energy.grid.grid_carbon_footprint" @@ -499,6 +553,97 @@ export class EnergyGridSettings extends LitElement { await this._savePreferences(cleanedPreferences); } + private _addPowerSource() { + const gridSource = this.preferences.energy_sources.find( + (src) => src.type === "grid" + ) as GridSourceTypeEnergyPreference | undefined; + showEnergySettingsGridPowerDialog(this, { + grid_source: gridSource, + saveCallback: async (power) => { + let preferences: EnergyPreferences; + if (!gridSource) { + preferences = { + ...this.preferences, + energy_sources: [ + ...this.preferences.energy_sources, + { + ...emptyGridSourceEnergyPreference(), + power: [power], + }, + ], + }; + } else { + preferences = { + ...this.preferences, + energy_sources: this.preferences.energy_sources.map((src) => + src.type === "grid" + ? { ...src, power: [...(gridSource.power || []), power] } + : src + ), + }; + } + await this._savePreferences(preferences); + }, + }); + } + + private _editPowerSource(ev) { + const origSource: GridPowerSourceEnergyPreference = + ev.currentTarget.closest(".row").source; + const gridSource = this.preferences.energy_sources.find( + (src) => src.type === "grid" + ) as GridSourceTypeEnergyPreference | undefined; + showEnergySettingsGridPowerDialog(this, { + source: { ...origSource }, + grid_source: gridSource, + saveCallback: async (source) => { + const power = + energySourcesByType(this.preferences).grid![0].power || []; + + const preferences: EnergyPreferences = { + ...this.preferences, + energy_sources: this.preferences.energy_sources.map((src) => + src.type === "grid" + ? { + ...src, + power: power.map((p) => (p === origSource ? source : p)), + } + : src + ), + }; + await this._savePreferences(preferences); + }, + }); + } + + private async _deletePowerSource(ev) { + const sourceToDelete: GridPowerSourceEnergyPreference = + ev.currentTarget.closest(".row").source; + + if ( + !(await showConfirmationDialog(this, { + title: this.hass.localize("ui.panel.config.energy.delete_source"), + })) + ) { + return; + } + + const power = + energySourcesByType(this.preferences).grid![0].power?.filter( + (p) => p !== sourceToDelete + ) || []; + + const preferences: EnergyPreferences = { + ...this.preferences, + energy_sources: this.preferences.energy_sources.map((source) => + source.type === "grid" ? { ...source, power } : source + ), + }; + + const cleanedPreferences = this._removeEmptySources(preferences); + await this._savePreferences(cleanedPreferences); + } + private _removeEmptySources(preferences: EnergyPreferences) { // Check if grid sources became an empty type and remove if so preferences.energy_sources = preferences.energy_sources.reduce< @@ -507,7 +652,8 @@ export class EnergyGridSettings extends LitElement { if ( source.type !== "grid" || source.flow_from.length > 0 || - source.flow_to.length > 0 + source.flow_to.length > 0 || + (source.power && source.power.length > 0) ) { acc.push(source); } diff --git a/src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts index 1ffe3249f16d..e341d94e49e9 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts @@ -18,6 +18,7 @@ import type { HomeAssistant } from "../../../../types"; import type { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy"; const energyUnitClasses = ["energy"]; +const powerUnitClasses = ["power"]; @customElement("dialog-energy-battery-settings") export class DialogEnergyBatterySettings @@ -32,10 +33,14 @@ export class DialogEnergyBatterySettings @state() private _energy_units?: string[]; + @state() private _power_units?: string[]; + @state() private _error?: string; private _excludeList?: string[]; + private _excludeListPower?: string[]; + public async showDialog( params: EnergySettingsBatteryDialogParams ): Promise { @@ -46,6 +51,9 @@ export class DialogEnergyBatterySettings this._energy_units = ( await getSensorDeviceClassConvertibleUnits(this.hass, "energy") ).units; + this._power_units = ( + await getSensorDeviceClassConvertibleUnits(this.hass, "power") + ).units; const allSources: string[] = []; this._params.battery_sources.forEach((entry) => { allSources.push(entry.stat_energy_from); @@ -56,6 +64,9 @@ export class DialogEnergyBatterySettings id !== this._source?.stat_energy_from && id !== this._source?.stat_energy_to ); + this._excludeListPower = this._params.battery_sources + .map((entry) => entry.stat_rate) + .filter((id) => id && id !== this._source?.stat_rate) as string[]; } public closeDialog() { @@ -72,8 +83,6 @@ export class DialogEnergyBatterySettings return nothing; } - const pickableUnit = this._energy_units?.join(", ") || ""; - return html` ${this._error ? html`

${this._error}

` : ""} -
- ${this.hass.localize( - "ui.panel.config.energy.battery.dialog.entity_para", - { unit: pickableUnit } - )} -
@@ -121,6 +128,25 @@ export class DialogEnergyBatterySettings this._source.stat_energy_to, ]} @value-changed=${this._statisticFromChanged} + .helper=${this.hass.localize( + "ui.panel.config.energy.battery.dialog.energy_helper_out", + { unit: this._energy_units?.join(", ") || "" } + )} + > + + ) { + this._source = { ...this._source!, stat_rate: ev.detail.value }; + } + private async _save() { try { await this._params!.saveCallback(this._source!); @@ -168,7 +198,11 @@ export class DialogEnergyBatterySettings --mdc-dialog-max-width: 430px; } ha-statistic-picker { - width: 100%; + display: block; + margin-bottom: var(--ha-space-4); + } + ha-statistic-picker:last-of-type { + margin-bottom: 0; } `, ]; diff --git a/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts index c6aceb608162..afc2d732aedf 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts @@ -21,6 +21,7 @@ import type { HomeAssistant } from "../../../../types"; import type { EnergySettingsDeviceDialogParams } from "./show-dialogs-energy"; const energyUnitClasses = ["energy"]; +const powerUnitClasses = ["power"]; @customElement("dialog-energy-device-settings") export class DialogEnergyDeviceSettings @@ -35,10 +36,14 @@ export class DialogEnergyDeviceSettings @state() private _energy_units?: string[]; + @state() private _power_units?: string[]; + @state() private _error?: string; private _excludeList?: string[]; + private _excludeListPower?: string[]; + private _possibleParents: DeviceConsumptionEnergyPreference[] = []; public async showDialog( @@ -50,9 +55,15 @@ export class DialogEnergyDeviceSettings this._energy_units = ( await getSensorDeviceClassConvertibleUnits(this.hass, "energy") ).units; + this._power_units = ( + await getSensorDeviceClassConvertibleUnits(this.hass, "power") + ).units; this._excludeList = this._params.device_consumptions .map((entry) => entry.stat_consumption) .filter((id) => id !== this._device?.stat_consumption); + this._excludeListPower = this._params.device_consumptions + .map((entry) => entry.stat_rate) + .filter((id) => id && id !== this._device?.stat_rate) as string[]; } private _computePossibleParents() { @@ -93,8 +104,6 @@ export class DialogEnergyDeviceSettings return nothing; } - const pickableUnit = this._energy_units?.join(", ") || ""; - return html` ${this._error ? html`

${this._error}

` : ""} -
- ${this.hass.localize( - "ui.panel.config.energy.device_consumption.dialog.selected_stat_intro", - { unit: pickableUnit } - )} -
+ + ) { + if (!this._device) { + return; + } + const newDevice = { + ...this._device, + stat_rate: ev.detail.value, + } as DeviceConsumptionEnergyPreference; + if (!newDevice.stat_rate) { + delete newDevice.stat_rate; + } + this._device = newDevice; + } + private _nameChanged(ev) { const newDevice = { ...this._device!, @@ -245,15 +281,19 @@ export class DialogEnergyDeviceSettings return [ haStyleDialog, css` + ha-statistic-picker { + display: block; + margin-bottom: var(--ha-space-2); + } ha-statistic-picker { width: 100%; } ha-select { - margin-top: 16px; + margin-top: var(--ha-space-4); width: 100%; } ha-textfield { - margin-top: 16px; + margin-top: var(--ha-space-4); width: 100%; } `, diff --git a/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts index d0fddbc8ba69..a48b7bd15a08 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts @@ -115,8 +115,6 @@ export class DialogEnergyGridFlowSettings return nothing; } - const pickableUnit = this._energy_units?.join(", ") || ""; - const unitPriceSensor = this._pickedDisplayUnit ? `${this.hass.config.currency}/${this._pickedDisplayUnit}` : undefined; @@ -150,19 +148,11 @@ export class DialogEnergyGridFlowSettings @closed=${this.closeDialog} > ${this._error ? html`

${this._error}

` : ""} -
-

- ${this.hass.localize( - `ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.paragraph` - )} -

-

- ${this.hass.localize( - `ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.entity_para`, - { unit: pickableUnit } - )} -

-
+

+ ${this.hass.localize( + `ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.paragraph` + )} +

@@ -380,6 +374,10 @@ export class DialogEnergyGridFlowSettings ha-dialog { --mdc-dialog-max-width: 430px; } + ha-statistic-picker { + display: block; + margin: var(--ha-space-4) 0; + } ha-formfield { display: block; } diff --git a/src/panels/config/energy/dialogs/dialog-energy-grid-power-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-grid-power-settings.ts new file mode 100644 index 000000000000..0410a34bab63 --- /dev/null +++ b/src/panels/config/energy/dialogs/dialog-energy-grid-power-settings.ts @@ -0,0 +1,153 @@ +import { mdiTransmissionTower } from "@mdi/js"; +import type { CSSResultGroup } from "lit"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/entity/ha-statistic-picker"; +import "../../../../components/ha-dialog"; +import "../../../../components/ha-button"; +import type { GridPowerSourceEnergyPreference } from "../../../../data/energy"; +import { energyStatisticHelpUrl } from "../../../../data/energy"; +import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor"; +import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; +import { haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import type { EnergySettingsGridPowerDialogParams } from "./show-dialogs-energy"; + +const powerUnitClasses = ["power"]; + +@customElement("dialog-energy-grid-power-settings") +export class DialogEnergyGridPowerSettings + extends LitElement + implements HassDialog +{ + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: EnergySettingsGridPowerDialogParams; + + @state() private _source?: GridPowerSourceEnergyPreference; + + @state() private _power_units?: string[]; + + @state() private _error?: string; + + private _excludeListPower?: string[]; + + public async showDialog( + params: EnergySettingsGridPowerDialogParams + ): Promise { + this._params = params; + this._source = params.source ? { ...params.source } : { stat_rate: "" }; + + const initialSourceIdPower = this._source.stat_rate; + + this._power_units = ( + await getSensorDeviceClassConvertibleUnits(this.hass, "power") + ).units; + + this._excludeListPower = [ + ...(this._params.grid_source?.power?.map((entry) => entry.stat_rate) || + []), + ].filter((id) => id && id !== initialSourceIdPower) as string[]; + } + + public closeDialog() { + this._params = undefined; + this._source = undefined; + this._error = undefined; + this._excludeListPower = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + return true; + } + + protected render() { + if (!this._params || !this._source) { + return nothing; + } + + return html` + ${this.hass.localize( + "ui.panel.config.energy.grid.power_dialog.header" + )}`} + @closed=${this.closeDialog} + > + ${this._error ? html`

${this._error}

` : ""} + + + + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize("ui.common.save")} + +
+ `; + } + + private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) { + this._source = { + ...this._source!, + stat_rate: ev.detail.value, + }; + } + + private async _save() { + try { + await this._params!.saveCallback(this._source!); + this.closeDialog(); + } catch (err: any) { + this._error = err.message; + } + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-dialog { + --mdc-dialog-max-width: 430px; + } + ha-statistic-picker { + display: block; + margin: var(--ha-space-4) 0; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-energy-grid-power-settings": DialogEnergyGridPowerSettings; + } +} diff --git a/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts index 46304a8b63ee..80a2f2312d63 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts @@ -28,6 +28,7 @@ import { brandsUrl } from "../../../../util/brands-url"; import type { EnergySettingsSolarDialogParams } from "./show-dialogs-energy"; const energyUnitClasses = ["energy"]; +const powerUnitClasses = ["power"]; @customElement("dialog-energy-solar-settings") export class DialogEnergySolarSettings @@ -46,10 +47,14 @@ export class DialogEnergySolarSettings @state() private _energy_units?: string[]; + @state() private _power_units?: string[]; + @state() private _error?: string; private _excludeList?: string[]; + private _excludeListPower?: string[]; + public async showDialog( params: EnergySettingsSolarDialogParams ): Promise { @@ -62,9 +67,15 @@ export class DialogEnergySolarSettings this._energy_units = ( await getSensorDeviceClassConvertibleUnits(this.hass, "energy") ).units; + this._power_units = ( + await getSensorDeviceClassConvertibleUnits(this.hass, "power") + ).units; this._excludeList = this._params.solar_sources .map((entry) => entry.stat_energy_from) .filter((id) => id !== this._source?.stat_energy_from); + this._excludeListPower = this._params.solar_sources + .map((entry) => entry.stat_rate) + .filter((id) => id && id !== this._source?.stat_rate) as string[]; } public closeDialog() { @@ -81,8 +92,6 @@ export class DialogEnergySolarSettings return nothing; } - const pickableUnit = this._energy_units?.join(", ") || ""; - return html` ${this._error ? html`

${this._error}

` : ""} -
- ${this.hass.localize( - "ui.panel.config.energy.solar.dialog.entity_para", - { unit: pickableUnit } - )} -
+ +

${this.hass.localize( "ui.panel.config.energy.solar.dialog.solar_production_forecast" @@ -267,6 +289,10 @@ export class DialogEnergySolarSettings this._source = { ...this._source!, stat_energy_from: ev.detail.value }; } + private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) { + this._source = { ...this._source!, stat_rate: ev.detail.value }; + } + private async _save() { try { if (!this._forecast) { @@ -287,6 +313,10 @@ export class DialogEnergySolarSettings ha-dialog { --mdc-dialog-max-width: 430px; } + ha-statistic-picker { + display: block; + margin-bottom: var(--ha-space-4); + } img { height: 24px; margin-right: 16px; diff --git a/src/panels/config/energy/dialogs/show-dialogs-energy.ts b/src/panels/config/energy/dialogs/show-dialogs-energy.ts index f85dc549e623..d2845e5e2622 100644 --- a/src/panels/config/energy/dialogs/show-dialogs-energy.ts +++ b/src/panels/config/energy/dialogs/show-dialogs-energy.ts @@ -7,6 +7,7 @@ import type { FlowFromGridSourceEnergyPreference, FlowToGridSourceEnergyPreference, GasSourceTypeEnergyPreference, + GridPowerSourceEnergyPreference, GridSourceTypeEnergyPreference, SolarSourceTypeEnergyPreference, WaterSourceTypeEnergyPreference, @@ -41,6 +42,12 @@ export interface EnergySettingsGridFlowToDialogParams { saveCallback: (source: FlowToGridSourceEnergyPreference) => Promise; } +export interface EnergySettingsGridPowerDialogParams { + source?: GridPowerSourceEnergyPreference; + grid_source?: GridSourceTypeEnergyPreference; + saveCallback: (source: GridPowerSourceEnergyPreference) => Promise; +} + export interface EnergySettingsSolarDialogParams { info: EnergyInfo; source?: SolarSourceTypeEnergyPreference; @@ -152,3 +159,14 @@ export const showEnergySettingsGridFlowToDialog = ( dialogParams: { ...dialogParams, direction: "to" }, }); }; + +export const showEnergySettingsGridPowerDialog = ( + element: HTMLElement, + dialogParams: EnergySettingsGridPowerDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-energy-grid-power-settings", + dialogImport: () => import("./dialog-energy-grid-power-settings"), + dialogParams: dialogParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 1a35e26eb67b..4cbd57e3a438 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3065,6 +3065,15 @@ "grid_carbon_footprint": "Grid carbon footprint", "remove_co2_signal": "Remove Electricity Maps integration", "add_co2_signal": "Add Electricity Maps integration", + "grid_power": "Grid power", + "add_power": "Add power sensor", + "edit_power": "Edit power sensor", + "delete_power": "Delete power sensor", + "power_dialog": { + "header": "Configure grid power", + "power_stat": "Power sensor", + "power_helper": "Pick a sensor which measures grid power in either of {unit}. Positive values indicate importing electricity from the grid, negative values indicate exporting electricity to the grid." + }, "flow_dialog": { "from": { "header": "Configure grid consumption", @@ -3111,6 +3120,7 @@ "header": "Configure solar panels", "entity_para": "Pick a sensor which measures solar energy production in either of {unit}.", "solar_production_energy": "Solar production energy", + "solar_production_power": "Solar production power", "solar_production_forecast": "Solar production forecast", "solar_production_forecast_description": "Adding solar production forecast information will allow you to quickly see your expected production for today.", "dont_forecast_production": "Don't forecast production", @@ -3128,9 +3138,12 @@ "add_battery_system": "Add battery system", "dialog": { "header": "Configure battery system", - "entity_para": "Pick sensors which measure energy going into and coming out of the battery in either of {unit}.", - "energy_into_battery": "Energy going into the battery", - "energy_out_of_battery": "Energy coming out of the battery" + "energy_helper_into": "Pick a sensor that measures the electricity flowing into the battery in either of {unit}.", + "energy_helper_out": "Pick a sensor that measures the electricity flowing out of the battery in either of {unit}.", + "energy_into_battery": "Energy charged into the battery", + "energy_out_of_battery": "Energy discharged from the battery", + "power": "Battery power", + "power_helper": "Pick a sensor which measures the electricity flowing into and out of the battery in either of {unit}. Positive values indicate discharging the battery, negative values indicate charging the battery." } }, "gas": { @@ -3192,7 +3205,8 @@ "header": "Add a device", "display_name": "Display name", "device_consumption_energy": "Device energy consumption", - "selected_stat_intro": "Select the energy sensor that measures the device's energy usage in either of {unit}.", + "device_consumption_power": "Device power consumption", + "selected_stat_intro": "Select the sensor that measures the device's electricity usage in either of {unit}.", "included_in_device": "Upstream device", "included_in_device_helper": "If this device is already counted by another device (such as a smart switch measured by a smart breaker), selecting the upstream device prevents duplicate energy tracking.", "no_upstream_devices": "No eligible upstream devices"