mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-17 15:45:43 +01:00
Migrate grid connections to single objects with import/export/power (#29389)
* Migrate grid connections to single objects with import/export/power * Fix duplicate imports in battery settings dialog * Update src/translations/en.json Co-authored-by: Norbert Rittel <norbert@rittel.de> * Update src/translations/en.json Co-authored-by: Norbert Rittel <norbert@rittel.de> * Remove redundant power grid translation keys from en.json * Remove redundant power charge and discharge keys from en.json * Clean up grid keys from en.json * Rename sell price to export * add descriptions * use ValueChangedEvent * Renamed translationKeyPrefix to localizeBaseKey * Add clarification to stat_rate in energy preference interfaces * Add handling for external statistics in energy cost tracking * Apply suggestion from @NoRi2909 Co-authored-by: Norbert Rittel <norbert@rittel.de> * Update src/translations/en.json Co-authored-by: Norbert Rittel <norbert@rittel.de> * update comments * Use ha-dialog instead of ha-wa-dialog --------- Co-authored-by: Norbert Rittel <norbert@rittel.de>
This commit is contained in:
@@ -14,40 +14,28 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||
energy_sources: [
|
||||
{
|
||||
type: "grid",
|
||||
flow_from: [
|
||||
{
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_1",
|
||||
stat_cost: "sensor.energy_consumption_tarif_1_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
{
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_2",
|
||||
stat_cost: "sensor.energy_consumption_tarif_2_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
],
|
||||
flow_to: [
|
||||
{
|
||||
stat_energy_to: "sensor.energy_production_tarif_1",
|
||||
stat_compensation:
|
||||
"sensor.energy_production_tarif_1_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
{
|
||||
stat_energy_to: "sensor.energy_production_tarif_2",
|
||||
stat_compensation:
|
||||
"sensor.energy_production_tarif_2_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
],
|
||||
power: [
|
||||
{ stat_rate: "sensor.power_grid" },
|
||||
{ stat_rate: "sensor.power_grid_return" },
|
||||
],
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_1",
|
||||
stat_energy_to: "sensor.energy_production_tarif_1",
|
||||
stat_cost: "sensor.energy_consumption_tarif_1_cost",
|
||||
stat_compensation: "sensor.energy_production_tarif_1_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
stat_rate: "sensor.power_grid",
|
||||
cost_adjustment_day: 0,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_2",
|
||||
stat_energy_to: "sensor.energy_production_tarif_2",
|
||||
stat_cost: "sensor.energy_consumption_tarif_2_cost",
|
||||
stat_compensation: "sensor.energy_production_tarif_2_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
stat_rate: "sensor.power_grid_return",
|
||||
cost_adjustment_day: 0,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -40,27 +40,17 @@ import { formatNumber } from "../common/number/format_number";
|
||||
|
||||
const energyCollectionKeys: (string | undefined)[] = [];
|
||||
|
||||
export const emptyFlowFromGridSourceEnergyPreference =
|
||||
(): FlowFromGridSourceEnergyPreference => ({
|
||||
stat_energy_from: "",
|
||||
stat_cost: null,
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
});
|
||||
|
||||
export const emptyFlowToGridSourceEnergyPreference =
|
||||
(): FlowToGridSourceEnergyPreference => ({
|
||||
stat_energy_to: "",
|
||||
stat_compensation: null,
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
});
|
||||
|
||||
export const emptyGridSourceEnergyPreference =
|
||||
(): GridSourceTypeEnergyPreference => ({
|
||||
type: "grid",
|
||||
flow_from: [],
|
||||
flow_to: [],
|
||||
stat_energy_from: null,
|
||||
stat_energy_to: null,
|
||||
stat_cost: null,
|
||||
stat_compensation: null,
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
cost_adjustment_day: 0,
|
||||
});
|
||||
|
||||
@@ -108,30 +98,6 @@ export interface DeviceConsumptionEnergyPreference {
|
||||
included_in_stat?: string;
|
||||
}
|
||||
|
||||
export interface FlowFromGridSourceEnergyPreference {
|
||||
// kWh meter
|
||||
stat_energy_from: string;
|
||||
|
||||
// $ meter
|
||||
stat_cost: string | null;
|
||||
|
||||
// Can be used to generate costs if stat_cost omitted
|
||||
entity_energy_price: string | null;
|
||||
number_energy_price: number | null;
|
||||
}
|
||||
|
||||
export interface FlowToGridSourceEnergyPreference {
|
||||
// kWh meter
|
||||
stat_energy_to: string;
|
||||
|
||||
// $ meter
|
||||
stat_compensation: string | null;
|
||||
|
||||
// Can be used to generate costs if stat_compensation omitted
|
||||
entity_energy_price: string | null;
|
||||
number_energy_price: number | null;
|
||||
}
|
||||
|
||||
export interface PowerConfig {
|
||||
stat_rate?: string; // Standard single sensor
|
||||
stat_rate_inverted?: string; // Inverted single sensor
|
||||
@@ -139,29 +105,33 @@ export interface PowerConfig {
|
||||
stat_rate_to?: string; // Battery: charge / Grid: return
|
||||
}
|
||||
|
||||
export interface GridPowerSourceEnergyPreference {
|
||||
stat_rate: string;
|
||||
power_config?: PowerConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input type for saving grid power sources.
|
||||
* Core requires EITHER stat_rate (legacy) OR power_config (new format).
|
||||
* When reading from backend, stat_rate is always populated.
|
||||
* Grid source format.
|
||||
* Each grid connection is a single object with import/export/power together.
|
||||
* Multiple grid sources are allowed.
|
||||
*/
|
||||
export type GridPowerSourceInput = Omit<
|
||||
GridPowerSourceEnergyPreference,
|
||||
"stat_rate"
|
||||
> & {
|
||||
stat_rate?: string;
|
||||
};
|
||||
|
||||
export interface GridSourceTypeEnergyPreference {
|
||||
type: "grid";
|
||||
|
||||
flow_from: FlowFromGridSourceEnergyPreference[];
|
||||
flow_to: FlowToGridSourceEnergyPreference[];
|
||||
power?: GridPowerSourceEnergyPreference[];
|
||||
// Import meter
|
||||
stat_energy_from: string | null;
|
||||
|
||||
// Export meter
|
||||
stat_energy_to: string | null;
|
||||
|
||||
// Import cost tracking
|
||||
stat_cost: string | null;
|
||||
entity_energy_price: string | null;
|
||||
number_energy_price: number | null;
|
||||
|
||||
// Export compensation tracking
|
||||
stat_compensation: string | null;
|
||||
entity_energy_price_export: string | null;
|
||||
number_energy_price_export: number | null;
|
||||
|
||||
// Power measurement
|
||||
stat_rate?: string; // always available if power_config is set
|
||||
power_config?: PowerConfig;
|
||||
|
||||
cost_adjustment_day: number;
|
||||
}
|
||||
@@ -178,7 +148,7 @@ export interface BatterySourceTypeEnergyPreference {
|
||||
type: "battery";
|
||||
stat_energy_from: string;
|
||||
stat_energy_to: string;
|
||||
stat_rate?: string;
|
||||
stat_rate?: string; // always available if power_config is set
|
||||
power_config?: PowerConfig;
|
||||
}
|
||||
export interface GasSourceTypeEnergyPreference {
|
||||
@@ -355,24 +325,25 @@ export const getReferencedStatisticIds = (
|
||||
}
|
||||
|
||||
// grid source
|
||||
for (const flowFrom of source.flow_from) {
|
||||
statIDs.push(flowFrom.stat_energy_from);
|
||||
if (flowFrom.stat_cost) {
|
||||
statIDs.push(flowFrom.stat_cost);
|
||||
if (source.stat_energy_from) {
|
||||
statIDs.push(source.stat_energy_from);
|
||||
if (source.stat_cost) {
|
||||
statIDs.push(source.stat_cost);
|
||||
}
|
||||
const costStatId = info.cost_sensors[flowFrom.stat_energy_from];
|
||||
if (costStatId) {
|
||||
statIDs.push(costStatId);
|
||||
const importCostStatId = info.cost_sensors[source.stat_energy_from];
|
||||
if (importCostStatId) {
|
||||
statIDs.push(importCostStatId);
|
||||
}
|
||||
}
|
||||
for (const flowTo of source.flow_to) {
|
||||
statIDs.push(flowTo.stat_energy_to);
|
||||
if (flowTo.stat_compensation) {
|
||||
statIDs.push(flowTo.stat_compensation);
|
||||
|
||||
if (source.stat_energy_to) {
|
||||
statIDs.push(source.stat_energy_to);
|
||||
if (source.stat_compensation) {
|
||||
statIDs.push(source.stat_compensation);
|
||||
}
|
||||
const costStatId = info.cost_sensors[flowTo.stat_energy_to];
|
||||
if (costStatId) {
|
||||
statIDs.push(costStatId);
|
||||
const exportCostStatId = info.cost_sensors[source.stat_energy_to];
|
||||
if (exportCostStatId) {
|
||||
statIDs.push(exportCostStatId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,12 +375,15 @@ export const getReferencedStatisticIdsPower = (
|
||||
}
|
||||
|
||||
if (source.type === "battery") {
|
||||
statIDs.push(source.stat_rate);
|
||||
if (source.stat_rate) {
|
||||
statIDs.push(source.stat_rate);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.power) {
|
||||
statIDs.push(...source.power.map((p) => p.stat_rate));
|
||||
// grid source
|
||||
if (source.stat_rate) {
|
||||
statIDs.push(source.stat_rate);
|
||||
}
|
||||
}
|
||||
statIDs.push(...prefs.device_consumption.map((d) => d.stat_rate));
|
||||
@@ -450,11 +424,8 @@ const getEnergyData = async (
|
||||
|
||||
const consumptionStatIDs: string[] = [];
|
||||
for (const source of prefs.energy_sources) {
|
||||
// grid source
|
||||
if (source.type === "grid") {
|
||||
for (const flowFrom of source.flow_from) {
|
||||
consumptionStatIDs.push(flowFrom.stat_energy_from);
|
||||
}
|
||||
if (source.type === "grid" && source.stat_energy_from) {
|
||||
consumptionStatIDs.push(source.stat_energy_from);
|
||||
}
|
||||
}
|
||||
const energyStatIds = getReferencedStatisticIds(prefs, info, [
|
||||
@@ -1055,18 +1026,18 @@ const getSummedDataPartial = (
|
||||
}
|
||||
|
||||
// grid source
|
||||
for (const flowFrom of source.flow_from) {
|
||||
if (source.stat_energy_from) {
|
||||
if (statIds.from_grid) {
|
||||
statIds.from_grid.push(flowFrom.stat_energy_from);
|
||||
statIds.from_grid.push(source.stat_energy_from);
|
||||
} else {
|
||||
statIds.from_grid = [flowFrom.stat_energy_from];
|
||||
statIds.from_grid = [source.stat_energy_from];
|
||||
}
|
||||
}
|
||||
for (const flowTo of source.flow_to) {
|
||||
if (source.stat_energy_to) {
|
||||
if (statIds.to_grid) {
|
||||
statIds.to_grid.push(flowTo.stat_energy_to);
|
||||
statIds.to_grid.push(source.stat_energy_to);
|
||||
} else {
|
||||
statIds.to_grid = [flowTo.stat_energy_to];
|
||||
statIds.to_grid = [source.stat_energy_to];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiHomeExportOutline,
|
||||
mdiHomeImportOutline,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
mdiTransmissionTower,
|
||||
} from "@mdi/js";
|
||||
import { mdiDelete, mdiPencil, mdiPlus, mdiTransmissionTower } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { html, LitElement } 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/ha-card";
|
||||
@@ -23,18 +16,9 @@ import type {
|
||||
EnergyPreferences,
|
||||
EnergyPreferencesValidation,
|
||||
EnergyValidationIssue,
|
||||
EnergySource,
|
||||
FlowFromGridSourceEnergyPreference,
|
||||
FlowToGridSourceEnergyPreference,
|
||||
GridPowerSourceEnergyPreference,
|
||||
GridPowerSourceInput,
|
||||
GridSourceTypeEnergyPreference,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
emptyGridSourceEnergyPreference,
|
||||
energySourcesByType,
|
||||
saveEnergyPreferences,
|
||||
} from "../../../../data/energy";
|
||||
import { saveEnergyPreferences } from "../../../../data/energy";
|
||||
import type { StatisticsMetaData } from "../../../../data/recorder";
|
||||
import { getStatisticLabel } from "../../../../data/recorder";
|
||||
import { showConfigFlowDialog } from "../../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
@@ -46,11 +30,7 @@ import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { brandsUrl } from "../../../../util/brands-url";
|
||||
import { documentationUrl } from "../../../../util/documentation-url";
|
||||
import {
|
||||
showEnergySettingsGridFlowFromDialog,
|
||||
showEnergySettingsGridFlowToDialog,
|
||||
showEnergySettingsGridPowerDialog,
|
||||
} from "../dialogs/show-dialogs-energy";
|
||||
import { showEnergySettingsGridDialog } from "../dialogs/show-dialogs-energy";
|
||||
import "./ha-energy-validation-result";
|
||||
import { energyCardStyles } from "./styles";
|
||||
|
||||
@@ -74,23 +54,19 @@ export class EnergyGridSettings extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const gridIdx = this.preferences.energy_sources.findIndex(
|
||||
(source) => source.type === "grid"
|
||||
);
|
||||
const gridSources: GridSourceTypeEnergyPreference[] = [];
|
||||
const gridValidation: EnergyValidationIssue[][] = [];
|
||||
|
||||
let gridSource: GridSourceTypeEnergyPreference;
|
||||
let gridValidation: EnergyValidationIssue[] | undefined;
|
||||
|
||||
if (gridIdx === -1) {
|
||||
gridSource = emptyGridSourceEnergyPreference();
|
||||
} else {
|
||||
gridSource = this.preferences.energy_sources[
|
||||
gridIdx
|
||||
] as GridSourceTypeEnergyPreference;
|
||||
if (this.validationResult) {
|
||||
gridValidation = this.validationResult.energy_sources[gridIdx];
|
||||
this.preferences.energy_sources.forEach((source, idx) => {
|
||||
if (source.type !== "grid") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
gridSources.push(source);
|
||||
|
||||
if (this.validationResult) {
|
||||
gridValidation.push(this.validationResult.energy_sources[idx]);
|
||||
}
|
||||
});
|
||||
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
@@ -114,154 +90,65 @@ export class EnergyGridSettings extends LitElement {
|
||||
)}</a
|
||||
>
|
||||
</p>
|
||||
${gridValidation
|
||||
? html`
|
||||
<ha-energy-validation-result
|
||||
.hass=${this.hass}
|
||||
.issues=${gridValidation}
|
||||
></ha-energy-validation-result>
|
||||
`
|
||||
: ""}
|
||||
${gridValidation.map(
|
||||
(result) => html`
|
||||
<ha-energy-validation-result
|
||||
.hass=${this.hass}
|
||||
.issues=${result}
|
||||
></ha-energy-validation-result>
|
||||
`
|
||||
)}
|
||||
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.grid_consumption"
|
||||
"ui.panel.config.energy.grid.grid_connections"
|
||||
)}
|
||||
</h3>
|
||||
${gridSource.flow_from.map((flow) => {
|
||||
const entityState = this.hass.states[flow.stat_energy_from];
|
||||
${gridSources.map((source, idx) => {
|
||||
// At least one of import/export/power must exist (enforced by validation)
|
||||
const primaryStat = (source.stat_energy_from ||
|
||||
source.stat_energy_to ||
|
||||
source.stat_rate)!;
|
||||
const primaryEntityState = this.hass.states[primaryStat];
|
||||
return html`
|
||||
<div class="row" .source=${flow}>
|
||||
${entityState?.attributes.icon
|
||||
<div class="row" .source=${source} .sourceIndex=${idx}>
|
||||
${primaryEntityState?.attributes.icon
|
||||
? html`<ha-icon
|
||||
.icon=${entityState?.attributes.icon}
|
||||
></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
.path=${mdiHomeImportOutline}
|
||||
></ha-svg-icon>`}
|
||||
<span class="content"
|
||||
>${getStatisticLabel(
|
||||
this.hass,
|
||||
flow.stat_energy_from,
|
||||
this.statsMetadata?.[flow.stat_energy_from]
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.edit_consumption"
|
||||
)}
|
||||
@click=${this._editFromSource}
|
||||
.path=${mdiPencil}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.delete_consumption"
|
||||
)}
|
||||
@click=${this._deleteFromSource}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
<div class="row border-bottom">
|
||||
<ha-svg-icon .path=${mdiHomeImportOutline}></ha-svg-icon>
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
size="small"
|
||||
@click=${this._addFromSource}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.add_consumption"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
${this.hass.localize("ui.panel.config.energy.grid.return_to_grid")}
|
||||
</h3>
|
||||
${gridSource.flow_to.map((flow) => {
|
||||
const entityState = this.hass.states[flow.stat_energy_to];
|
||||
return html`
|
||||
<div class="row" .source=${flow}>
|
||||
${entityState?.attributes.icon
|
||||
? html`<ha-icon
|
||||
.icon=${entityState.attributes.icon}
|
||||
></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
.path=${mdiHomeExportOutline}
|
||||
></ha-svg-icon>`}
|
||||
<span class="content"
|
||||
>${getStatisticLabel(
|
||||
this.hass,
|
||||
flow.stat_energy_to,
|
||||
this.statsMetadata?.[flow.stat_energy_to]
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.edit_return"
|
||||
)}
|
||||
@click=${this._editToSource}
|
||||
.path=${mdiPencil}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.delete_return"
|
||||
)}
|
||||
@click=${this._deleteToSource}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
<div class="row border-bottom">
|
||||
<ha-svg-icon .path=${mdiHomeExportOutline}></ha-svg-icon>
|
||||
<ha-button
|
||||
@click=${this._addToSource}
|
||||
appearance="filled"
|
||||
size="small"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.add_return"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
${this.hass.localize("ui.panel.config.energy.grid.grid_power")}
|
||||
</h3>
|
||||
${gridSource.power?.map((power) => {
|
||||
const entityState = this.hass.states[power.stat_rate];
|
||||
return html`
|
||||
<div class="row" .source=${power}>
|
||||
${entityState?.attributes.icon
|
||||
? html`<ha-icon
|
||||
.icon=${entityState.attributes.icon}
|
||||
.icon=${primaryEntityState.attributes.icon}
|
||||
></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
.path=${mdiTransmissionTower}
|
||||
></ha-svg-icon>`}
|
||||
<span class="content"
|
||||
>${getStatisticLabel(
|
||||
this.hass,
|
||||
power.stat_rate,
|
||||
this.statsMetadata?.[power.stat_rate]
|
||||
)}</span
|
||||
>
|
||||
<div class="content">
|
||||
<span class="label"
|
||||
>${getStatisticLabel(
|
||||
this.hass,
|
||||
primaryStat,
|
||||
this.statsMetadata?.[primaryStat]
|
||||
)}</span
|
||||
>
|
||||
${source.stat_energy_from && source.stat_energy_to
|
||||
? html`<span class="label secondary"
|
||||
>${getStatisticLabel(
|
||||
this.hass,
|
||||
source.stat_energy_to,
|
||||
this.statsMetadata?.[source.stat_energy_to]
|
||||
)}</span
|
||||
>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.edit_power"
|
||||
"ui.panel.config.energy.grid.edit_connection"
|
||||
)}
|
||||
@click=${this._editPowerSource}
|
||||
@click=${this._editSource}
|
||||
.path=${mdiPencil}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.delete_power"
|
||||
"ui.panel.config.energy.grid.delete_connection"
|
||||
)}
|
||||
@click=${this._deletePowerSource}
|
||||
@click=${this._deleteSource}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
@@ -270,13 +157,13 @@ export class EnergyGridSettings extends LitElement {
|
||||
<div class="row border-bottom">
|
||||
<ha-svg-icon .path=${mdiTransmissionTower}></ha-svg-icon>
|
||||
<ha-button
|
||||
@click=${this._addPowerSource}
|
||||
@click=${this._addSource}
|
||||
appearance="filled"
|
||||
size="small"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.add_power"
|
||||
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.add_connection"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
@@ -369,140 +256,53 @@ export class EnergyGridSettings extends LitElement {
|
||||
this._fetchCO2SignalConfigEntries();
|
||||
}
|
||||
|
||||
private _addFromSource() {
|
||||
const gridSource = this.preferences.energy_sources.find(
|
||||
(src) => src.type === "grid"
|
||||
) as GridSourceTypeEnergyPreference | undefined;
|
||||
showEnergySettingsGridFlowFromDialog(this, {
|
||||
grid_source: gridSource,
|
||||
saveCallback: async (flow) => {
|
||||
let preferences: EnergyPreferences;
|
||||
if (!gridSource) {
|
||||
preferences = {
|
||||
...this.preferences,
|
||||
energy_sources: [
|
||||
...this.preferences.energy_sources,
|
||||
{
|
||||
...emptyGridSourceEnergyPreference(),
|
||||
flow_from: [flow],
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
preferences = {
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.map((src) =>
|
||||
src.type === "grid"
|
||||
? { ...src, flow_from: [...gridSource.flow_from, flow] }
|
||||
: src
|
||||
),
|
||||
};
|
||||
}
|
||||
await this._savePreferences(preferences);
|
||||
},
|
||||
});
|
||||
private _getGridSources(): GridSourceTypeEnergyPreference[] {
|
||||
return this.preferences.energy_sources.filter(
|
||||
(src): src is GridSourceTypeEnergyPreference => src.type === "grid"
|
||||
);
|
||||
}
|
||||
|
||||
private _addToSource() {
|
||||
const gridSource = this.preferences.energy_sources.find(
|
||||
(src) => src.type === "grid"
|
||||
) as GridSourceTypeEnergyPreference | undefined;
|
||||
showEnergySettingsGridFlowToDialog(this, {
|
||||
grid_source: gridSource,
|
||||
saveCallback: async (flow) => {
|
||||
let preferences: EnergyPreferences;
|
||||
if (!gridSource) {
|
||||
preferences = {
|
||||
...this.preferences,
|
||||
energy_sources: [
|
||||
...this.preferences.energy_sources,
|
||||
{
|
||||
...emptyGridSourceEnergyPreference(),
|
||||
flow_to: [flow],
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
preferences = {
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.map((src) =>
|
||||
src.type === "grid"
|
||||
? { ...src, flow_to: [...gridSource.flow_to, flow] }
|
||||
: src
|
||||
),
|
||||
};
|
||||
}
|
||||
await this._savePreferences(preferences);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _editFromSource(ev) {
|
||||
const origSource: FlowFromGridSourceEnergyPreference =
|
||||
ev.currentTarget.closest(".row").source;
|
||||
const gridSource = this.preferences.energy_sources.find(
|
||||
(src) => src.type === "grid"
|
||||
) as GridSourceTypeEnergyPreference | undefined;
|
||||
showEnergySettingsGridFlowFromDialog(this, {
|
||||
source: { ...origSource },
|
||||
grid_source: gridSource,
|
||||
metadata: this.statsMetadata?.[origSource.stat_energy_from],
|
||||
private _addSource() {
|
||||
showEnergySettingsGridDialog(this, {
|
||||
grid_sources: this._getGridSources(),
|
||||
saveCallback: async (source) => {
|
||||
const flowFrom = energySourcesByType(this.preferences).grid![0]
|
||||
.flow_from;
|
||||
|
||||
const preferences: EnergyPreferences = {
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.map((src) =>
|
||||
src.type === "grid"
|
||||
? {
|
||||
...src,
|
||||
flow_from: flowFrom.map((flow) =>
|
||||
flow === origSource ? source : flow
|
||||
),
|
||||
}
|
||||
: src
|
||||
),
|
||||
energy_sources: [...this.preferences.energy_sources, source],
|
||||
};
|
||||
await this._savePreferences(preferences);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _editToSource(ev) {
|
||||
const origSource: FlowToGridSourceEnergyPreference =
|
||||
ev.currentTarget.closest(".row").source;
|
||||
const gridSource = this.preferences.energy_sources.find(
|
||||
(src) => src.type === "grid"
|
||||
) as GridSourceTypeEnergyPreference | undefined;
|
||||
showEnergySettingsGridFlowToDialog(this, {
|
||||
private _editSource(ev) {
|
||||
const row = ev.currentTarget.closest(".row");
|
||||
const origSource: GridSourceTypeEnergyPreference = row.source;
|
||||
const sourceIndex: number = row.sourceIndex;
|
||||
|
||||
showEnergySettingsGridDialog(this, {
|
||||
source: { ...origSource },
|
||||
grid_source: gridSource,
|
||||
metadata: this.statsMetadata?.[origSource.stat_energy_to],
|
||||
saveCallback: async (source) => {
|
||||
const flowTo = energySourcesByType(this.preferences).grid![0].flow_to;
|
||||
grid_sources: this._getGridSources(),
|
||||
saveCallback: async (newSource) => {
|
||||
const nonGridSources = this.preferences.energy_sources.filter(
|
||||
(src) => src.type !== "grid"
|
||||
);
|
||||
const updatedGrids = this._getGridSources().map((src, idx) =>
|
||||
idx === sourceIndex ? newSource : src
|
||||
);
|
||||
|
||||
const preferences: EnergyPreferences = {
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.map((src) =>
|
||||
src.type === "grid"
|
||||
? {
|
||||
...src,
|
||||
flow_to: flowTo.map((flow) =>
|
||||
flow === origSource ? source : flow
|
||||
),
|
||||
}
|
||||
: src
|
||||
),
|
||||
energy_sources: [...nonGridSources, ...updatedGrids],
|
||||
};
|
||||
await this._savePreferences(preferences);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _deleteFromSource(ev) {
|
||||
const sourceToDelete: FlowFromGridSourceEnergyPreference =
|
||||
ev.currentTarget.closest(".row").source;
|
||||
private async _deleteSource(ev) {
|
||||
const row = ev.currentTarget.closest(".row");
|
||||
const sourceIndex: number = row.sourceIndex;
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
@@ -512,166 +312,18 @@ export class EnergyGridSettings extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const flowFrom = energySourcesByType(
|
||||
this.preferences
|
||||
).grid![0].flow_from.filter((flow) => flow !== sourceToDelete);
|
||||
const nonGridSources = this.preferences.energy_sources.filter(
|
||||
(src) => src.type !== "grid"
|
||||
);
|
||||
const updatedGrids = this._getGridSources().filter(
|
||||
(_, idx) => idx !== sourceIndex
|
||||
);
|
||||
|
||||
const preferences: EnergyPreferences = {
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.map((source) =>
|
||||
source.type === "grid" ? { ...source, flow_from: flowFrom } : source
|
||||
),
|
||||
energy_sources: [...nonGridSources, ...updatedGrids],
|
||||
};
|
||||
|
||||
const cleanedPreferences = this._removeEmptySources(preferences);
|
||||
await this._savePreferences(cleanedPreferences);
|
||||
}
|
||||
|
||||
private async _deleteToSource(ev) {
|
||||
const sourceToDelete: FlowToGridSourceEnergyPreference =
|
||||
ev.currentTarget.closest(".row").source;
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.energy.delete_source"),
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const flowTo = energySourcesByType(
|
||||
this.preferences
|
||||
).grid![0].flow_to.filter((flow) => flow !== sourceToDelete);
|
||||
|
||||
const preferences: EnergyPreferences = {
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.map((source) =>
|
||||
source.type === "grid" ? { ...source, flow_to: flowTo } : source
|
||||
),
|
||||
};
|
||||
|
||||
const cleanedPreferences = this._removeEmptySources(preferences);
|
||||
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: GridPowerSourceInput) => {
|
||||
let preferences: EnergyPreferences;
|
||||
if (!gridSource) {
|
||||
preferences = {
|
||||
...this.preferences,
|
||||
energy_sources: [
|
||||
...this.preferences.energy_sources,
|
||||
{
|
||||
...emptyGridSourceEnergyPreference(),
|
||||
power: [power as GridPowerSourceEnergyPreference],
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
preferences = {
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.map((src) =>
|
||||
src.type === "grid"
|
||||
? {
|
||||
...src,
|
||||
power: [
|
||||
...(gridSource.power || []),
|
||||
power as GridPowerSourceEnergyPreference,
|
||||
],
|
||||
}
|
||||
: 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: GridPowerSourceInput) => {
|
||||
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 as GridPowerSourceEnergyPreference)
|
||||
: 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<
|
||||
EnergySource[]
|
||||
>((acc, source) => {
|
||||
if (
|
||||
source.type !== "grid" ||
|
||||
source.flow_from.length > 0 ||
|
||||
source.flow_to.length > 0 ||
|
||||
(source.power && source.power.length > 0)
|
||||
) {
|
||||
acc.push(source);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return preferences;
|
||||
await this._savePreferences(preferences);
|
||||
}
|
||||
|
||||
private async _savePreferences(preferences: EnergyPreferences) {
|
||||
@@ -684,7 +336,28 @@ export class EnergyGridSettings extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [haStyle, energyCardStyles];
|
||||
return [
|
||||
haStyle,
|
||||
energyCardStyles,
|
||||
css`
|
||||
.row {
|
||||
height: 58px;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.label.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,8 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-dialog";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import type {
|
||||
BatterySourceTypeEnergyPreference,
|
||||
PowerConfig,
|
||||
@@ -21,12 +18,17 @@ import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import "./ha-energy-power-config";
|
||||
import {
|
||||
buildPowerExcludeList,
|
||||
getInitialPowerConfig,
|
||||
getPowerTypeFromConfig,
|
||||
type HaEnergyPowerConfig,
|
||||
type PowerType,
|
||||
} from "./ha-energy-power-config";
|
||||
import type { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy";
|
||||
|
||||
const energyUnitClasses = ["energy"];
|
||||
const powerUnitClasses = ["power"];
|
||||
|
||||
type PowerType = "none" | "standard" | "inverted" | "two_sensors";
|
||||
|
||||
@customElement("dialog-energy-battery-settings")
|
||||
export class DialogEnergyBatterySettings
|
||||
@@ -47,8 +49,6 @@ export class DialogEnergyBatterySettings
|
||||
|
||||
@state() private _energy_units?: string[];
|
||||
|
||||
@state() private _power_units?: string[];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _excludeList?: string[];
|
||||
@@ -63,34 +63,19 @@ export class DialogEnergyBatterySettings
|
||||
? { ...params.source }
|
||||
: emptyBatteryEnergyPreference();
|
||||
|
||||
// Initialize power type from existing config
|
||||
if (params.source?.power_config) {
|
||||
const pc = params.source.power_config;
|
||||
this._powerConfig = { ...pc };
|
||||
if (pc.stat_rate_inverted) {
|
||||
this._powerType = "inverted";
|
||||
} else if (pc.stat_rate_from || pc.stat_rate_to) {
|
||||
this._powerType = "two_sensors";
|
||||
} else if (pc.stat_rate) {
|
||||
this._powerType = "standard";
|
||||
} else {
|
||||
this._powerType = "none";
|
||||
}
|
||||
} else if (params.source?.stat_rate) {
|
||||
// Legacy format - treat as standard
|
||||
this._powerType = "standard";
|
||||
this._powerConfig = { stat_rate: params.source.stat_rate };
|
||||
} else {
|
||||
this._powerType = "none";
|
||||
this._powerConfig = {};
|
||||
}
|
||||
// Initialize power type and config from existing source
|
||||
this._powerType = getPowerTypeFromConfig(
|
||||
params.source?.power_config,
|
||||
params.source?.stat_rate
|
||||
);
|
||||
this._powerConfig = getInitialPowerConfig(
|
||||
params.source?.power_config,
|
||||
params.source?.stat_rate
|
||||
);
|
||||
|
||||
this._energy_units = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
||||
).units;
|
||||
this._power_units = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
|
||||
).units;
|
||||
|
||||
// Build energy exclude list
|
||||
const allSources: string[] = [];
|
||||
@@ -104,32 +89,11 @@ export class DialogEnergyBatterySettings
|
||||
id !== this._source?.stat_energy_to
|
||||
);
|
||||
|
||||
// Build power exclude list
|
||||
const powerIds: string[] = [];
|
||||
this._params.battery_sources.forEach((entry) => {
|
||||
if (entry.stat_rate) powerIds.push(entry.stat_rate);
|
||||
if (entry.power_config) {
|
||||
if (entry.power_config.stat_rate)
|
||||
powerIds.push(entry.power_config.stat_rate);
|
||||
if (entry.power_config.stat_rate_inverted)
|
||||
powerIds.push(entry.power_config.stat_rate_inverted);
|
||||
if (entry.power_config.stat_rate_from)
|
||||
powerIds.push(entry.power_config.stat_rate_from);
|
||||
if (entry.power_config.stat_rate_to)
|
||||
powerIds.push(entry.power_config.stat_rate_to);
|
||||
}
|
||||
});
|
||||
|
||||
const currentPowerIds = [
|
||||
this._powerConfig.stat_rate,
|
||||
this._powerConfig.stat_rate_inverted,
|
||||
this._powerConfig.stat_rate_from,
|
||||
this._powerConfig.stat_rate_to,
|
||||
params.source?.stat_rate,
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
this._excludeListPower = powerIds.filter(
|
||||
(id) => !currentPowerIds.includes(id)
|
||||
// Build power exclude list using shared helper
|
||||
this._excludeListPower = buildPowerExcludeList(
|
||||
this._params.battery_sources,
|
||||
this._powerConfig,
|
||||
params.source?.stat_rate
|
||||
);
|
||||
|
||||
this._open = true;
|
||||
@@ -206,126 +170,14 @@ export class DialogEnergyBatterySettings
|
||||
)}
|
||||
></ha-statistic-picker>
|
||||
|
||||
<p class="power-section-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.sensor_type"
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.type_none"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="none"
|
||||
name="powerType"
|
||||
.checked=${this._powerType === "none"}
|
||||
@change=${this._handlePowerTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.type_standard"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="standard"
|
||||
name="powerType"
|
||||
.checked=${this._powerType === "standard"}
|
||||
@change=${this._handlePowerTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.type_inverted"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="inverted"
|
||||
name="powerType"
|
||||
.checked=${this._powerType === "inverted"}
|
||||
@change=${this._handlePowerTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.type_two_sensors"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="two_sensors"
|
||||
name="powerType"
|
||||
.checked=${this._powerType === "two_sensors"}
|
||||
@change=${this._handlePowerTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
|
||||
${this._powerType === "standard"
|
||||
? html`
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this._powerConfig.stat_rate}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.power"
|
||||
)}
|
||||
.excludeStatistics=${this._excludeListPower}
|
||||
@value-changed=${this._standardPowerChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.power_helper",
|
||||
{ unit: this._power_units?.join(", ") || "" }
|
||||
)}
|
||||
></ha-statistic-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._powerType === "inverted"
|
||||
? html`
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this._powerConfig.stat_rate_inverted}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.power"
|
||||
)}
|
||||
.excludeStatistics=${this._excludeListPower}
|
||||
@value-changed=${this._invertedPowerChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.type_inverted_description"
|
||||
)}
|
||||
></ha-statistic-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._powerType === "two_sensors"
|
||||
? html`
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this._powerConfig.stat_rate_from}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.power_discharge"
|
||||
)}
|
||||
.excludeStatistics=${[
|
||||
...(this._excludeListPower || []),
|
||||
this._powerConfig.stat_rate_to,
|
||||
].filter((id): id is string => Boolean(id))}
|
||||
@value-changed=${this._dischargePowerChanged}
|
||||
></ha-statistic-picker>
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this._powerConfig.stat_rate_to}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.power_charge"
|
||||
)}
|
||||
.excludeStatistics=${[
|
||||
...(this._excludeListPower || []),
|
||||
this._powerConfig.stat_rate_from,
|
||||
].filter((id): id is string => Boolean(id))}
|
||||
@value-changed=${this._chargePowerChanged}
|
||||
></ha-statistic-picker>
|
||||
`
|
||||
: nothing}
|
||||
<ha-energy-power-config
|
||||
.hass=${this.hass}
|
||||
.powerType=${this._powerType}
|
||||
.powerConfig=${this._powerConfig}
|
||||
.excludeList=${this._excludeListPower}
|
||||
localizeBaseKey="ui.panel.config.energy.battery.dialog"
|
||||
@power-config-changed=${this._handlePowerConfigChanged}
|
||||
></ha-energy-power-config>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
@@ -353,21 +205,15 @@ export class DialogEnergyBatterySettings
|
||||
return false;
|
||||
}
|
||||
|
||||
// Power fields depend on selected type
|
||||
switch (this._powerType) {
|
||||
case "none":
|
||||
return true;
|
||||
case "standard":
|
||||
return !!this._powerConfig.stat_rate;
|
||||
case "inverted":
|
||||
return !!this._powerConfig.stat_rate_inverted;
|
||||
case "two_sensors":
|
||||
return (
|
||||
!!this._powerConfig.stat_rate_from && !!this._powerConfig.stat_rate_to
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
// Check power config validity
|
||||
const powerConfigEl = this.shadowRoot?.querySelector(
|
||||
"ha-energy-power-config"
|
||||
) as HaEnergyPowerConfig | null;
|
||||
if (powerConfigEl && !powerConfigEl.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _statisticToChanged(ev: ValueChangedEvent<string>) {
|
||||
@@ -378,37 +224,11 @@ export class DialogEnergyBatterySettings
|
||||
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
|
||||
}
|
||||
|
||||
private _handlePowerTypeChanged(ev: Event) {
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
this._powerType = input.value as PowerType;
|
||||
// Clear power config when switching types
|
||||
this._powerConfig = {};
|
||||
}
|
||||
|
||||
private _standardPowerChanged(ev: ValueChangedEvent<string>) {
|
||||
this._powerConfig = {
|
||||
stat_rate: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
private _invertedPowerChanged(ev: ValueChangedEvent<string>) {
|
||||
this._powerConfig = {
|
||||
stat_rate_inverted: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
private _dischargePowerChanged(ev: ValueChangedEvent<string>) {
|
||||
this._powerConfig = {
|
||||
...this._powerConfig,
|
||||
stat_rate_from: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
private _chargePowerChanged(ev: ValueChangedEvent<string>) {
|
||||
this._powerConfig = {
|
||||
...this._powerConfig,
|
||||
stat_rate_to: ev.detail.value,
|
||||
};
|
||||
private _handlePowerConfigChanged(
|
||||
ev: CustomEvent<{ powerType: PowerType; powerConfig: PowerConfig }>
|
||||
) {
|
||||
this._powerType = ev.detail.powerType;
|
||||
this._powerConfig = ev.detail.powerConfig;
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
@@ -443,13 +263,6 @@ export class DialogEnergyBatterySettings
|
||||
ha-statistic-picker:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
.power-section-label {
|
||||
margin-top: var(--ha-space-4);
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,392 +0,0 @@
|
||||
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-entity-picker";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-markdown";
|
||||
import "../../../../components/ha-dialog";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import type {
|
||||
FlowFromGridSourceEnergyPreference,
|
||||
FlowToGridSourceEnergyPreference,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
emptyFlowFromGridSourceEnergyPreference,
|
||||
emptyFlowToGridSourceEnergyPreference,
|
||||
energyStatisticHelpUrl,
|
||||
} from "../../../../data/energy";
|
||||
import { isExternalStatistic } from "../../../../data/recorder";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import type { EnergySettingsGridFlowDialogParams } from "./show-dialogs-energy";
|
||||
|
||||
const energyUnitClasses = ["energy"];
|
||||
|
||||
@customElement("dialog-energy-grid-flow-settings")
|
||||
export class DialogEnergyGridFlowSettings
|
||||
extends LitElement
|
||||
implements HassDialog<EnergySettingsGridFlowDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: EnergySettingsGridFlowDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _source?:
|
||||
| FlowFromGridSourceEnergyPreference
|
||||
| FlowToGridSourceEnergyPreference;
|
||||
|
||||
@state() private _costs?: "no-costs" | "number" | "entity" | "statistic";
|
||||
|
||||
@state() private _energy_units?: string[];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _excludeList?: string[];
|
||||
|
||||
public async showDialog(
|
||||
params: EnergySettingsGridFlowDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._source = params.source
|
||||
? { ...params.source }
|
||||
: params.direction === "from"
|
||||
? emptyFlowFromGridSourceEnergyPreference()
|
||||
: emptyFlowToGridSourceEnergyPreference();
|
||||
this._costs = this._source.entity_energy_price
|
||||
? "entity"
|
||||
: this._source.number_energy_price
|
||||
? "number"
|
||||
: this._source[
|
||||
params.direction === "from" ? "stat_cost" : "stat_compensation"
|
||||
]
|
||||
? "statistic"
|
||||
: "no-costs";
|
||||
|
||||
const initialSourceId =
|
||||
this._source[
|
||||
this._params.direction === "from"
|
||||
? "stat_energy_from"
|
||||
: "stat_energy_to"
|
||||
];
|
||||
|
||||
this._energy_units = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
||||
).units;
|
||||
|
||||
this._excludeList = [
|
||||
...(this._params.grid_source?.flow_from?.map(
|
||||
(entry) => entry.stat_energy_from
|
||||
) || []),
|
||||
...(this._params.grid_source?.flow_to?.map(
|
||||
(entry) => entry.stat_energy_to
|
||||
) || []),
|
||||
].filter((id) => id !== initialSourceId);
|
||||
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._params = undefined;
|
||||
this._source = undefined;
|
||||
this._error = undefined;
|
||||
this._excludeList = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params || !this._source) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const unitPriceFixed = `${this.hass.config.currency}/kWh`;
|
||||
|
||||
const externalSource =
|
||||
this._source[
|
||||
this._params.direction === "from"
|
||||
? "stat_energy_from"
|
||||
: "stat_energy_to"
|
||||
] &&
|
||||
isExternalStatistic(
|
||||
this._source[
|
||||
this._params.direction === "from"
|
||||
? "stat_energy_from"
|
||||
: "stat_energy_to"
|
||||
]
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.header`
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.paragraph`
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||
.includeUnitClass=${energyUnitClasses}
|
||||
.value=${this._source[
|
||||
this._params.direction === "from"
|
||||
? "stat_energy_from"
|
||||
: "stat_energy_to"
|
||||
]}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.energy_stat`
|
||||
)}
|
||||
.excludeStatistics=${this._excludeList}
|
||||
@value-changed=${this._statisticChanged}
|
||||
.helper=${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.entity_para`,
|
||||
{ unit: this._energy_units?.join(", ") || "" }
|
||||
)}
|
||||
autofocus
|
||||
></ha-statistic-picker>
|
||||
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_para`
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.no_cost`
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="no-costs"
|
||||
name="costs"
|
||||
.checked=${this._costs === "no-costs"}
|
||||
@change=${this._handleCostChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_stat`
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="statistic"
|
||||
name="costs"
|
||||
.checked=${this._costs === "statistic"}
|
||||
@change=${this._handleCostChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
${this._costs === "statistic"
|
||||
? html`<ha-statistic-picker
|
||||
class="price-options"
|
||||
.hass=${this.hass}
|
||||
statistic-types="sum"
|
||||
.value=${this._source[
|
||||
this._params!.direction === "from"
|
||||
? "stat_cost"
|
||||
: "stat_compensation"
|
||||
]}
|
||||
.label=${`${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_stat_input`
|
||||
)} (${this.hass.config.currency})`}
|
||||
@value-changed=${this._priceStatChanged}
|
||||
></ha-statistic-picker>`
|
||||
: ""}
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_entity`
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="entity"
|
||||
name="costs"
|
||||
.checked=${this._costs === "entity"}
|
||||
.disabled=${externalSource}
|
||||
@change=${this._handleCostChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
${this._costs === "entity"
|
||||
? html`<ha-entity-picker
|
||||
class="price-options"
|
||||
.hass=${this.hass}
|
||||
include-domains='["sensor", "input_number"]'
|
||||
.value=${this._source.entity_energy_price}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_entity_input`
|
||||
)}
|
||||
.helper=${html`<ha-markdown
|
||||
.content=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.flow_dialog.cost_entity_helper",
|
||||
{ currency: this.hass.config.currency }
|
||||
)}
|
||||
></ha-markdown>`}
|
||||
@value-changed=${this._priceEntityChanged}
|
||||
></ha-entity-picker>`
|
||||
: ""}
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_number`
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="number"
|
||||
name="costs"
|
||||
.checked=${this._costs === "number"}
|
||||
.disabled=${externalSource}
|
||||
@change=${this._handleCostChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
${this._costs === "number"
|
||||
? html`<ha-textfield
|
||||
.label=${`${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_number_input`
|
||||
)} (${unitPriceFixed})`}
|
||||
class="price-options"
|
||||
step="any"
|
||||
type="number"
|
||||
.value=${this._source.number_energy_price}
|
||||
.suffix=${unitPriceFixed}
|
||||
@change=${this._numberPriceChanged}
|
||||
>
|
||||
</ha-textfield>`
|
||||
: ""}
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
slot="secondaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._source[
|
||||
this._params!.direction === "from"
|
||||
? "stat_energy_from"
|
||||
: "stat_energy_to"
|
||||
]}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleCostChanged(ev: CustomEvent) {
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
this._costs = input.value as any;
|
||||
}
|
||||
|
||||
private set _costStat(value: null | string) {
|
||||
this._source![
|
||||
this._params!.direction === "from" ? "stat_cost" : "stat_compensation"
|
||||
] = value;
|
||||
}
|
||||
|
||||
private _numberPriceChanged(ev: CustomEvent) {
|
||||
this._costStat = null;
|
||||
this._source = {
|
||||
...this._source!,
|
||||
number_energy_price: Number((ev.target as any).value),
|
||||
entity_energy_price: null,
|
||||
};
|
||||
}
|
||||
|
||||
private _priceStatChanged(ev: CustomEvent) {
|
||||
this._costStat = ev.detail.value;
|
||||
this._source = {
|
||||
...this._source!,
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
};
|
||||
}
|
||||
|
||||
private _priceEntityChanged(ev: CustomEvent) {
|
||||
this._costStat = null;
|
||||
this._source = {
|
||||
...this._source!,
|
||||
entity_energy_price: ev.detail.value,
|
||||
number_energy_price: null,
|
||||
};
|
||||
}
|
||||
|
||||
private async _statisticChanged(ev: ValueChangedEvent<string>) {
|
||||
if (
|
||||
ev.detail.value &&
|
||||
isExternalStatistic(ev.detail.value) &&
|
||||
this._costs !== "statistic"
|
||||
) {
|
||||
this._costs = "no-costs";
|
||||
}
|
||||
this._source = {
|
||||
...this._source!,
|
||||
[this._params!.direction === "from"
|
||||
? "stat_energy_from"
|
||||
: "stat_energy_to"]: 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._costStat = null;
|
||||
}
|
||||
await this._params!.saveCallback(this._source!);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-statistic-picker {
|
||||
display: block;
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
.price-options {
|
||||
display: block;
|
||||
padding-left: 52px;
|
||||
padding-inline-start: 52px;
|
||||
padding-inline-end: initial;
|
||||
margin-top: -8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-energy-grid-flow-settings": DialogEnergyGridFlowSettings;
|
||||
}
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
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-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-dialog";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import type {
|
||||
GridPowerSourceInput,
|
||||
PowerConfig,
|
||||
} 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, ValueChangedEvent } from "../../../../types";
|
||||
import type { EnergySettingsGridPowerDialogParams } from "./show-dialogs-energy";
|
||||
|
||||
const powerUnitClasses = ["power"];
|
||||
|
||||
type SensorType = "standard" | "inverted" | "two_sensors";
|
||||
|
||||
@customElement("dialog-energy-grid-power-settings")
|
||||
export class DialogEnergyGridPowerSettings
|
||||
extends LitElement
|
||||
implements HassDialog<EnergySettingsGridPowerDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: EnergySettingsGridPowerDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _sensorType: SensorType = "standard";
|
||||
|
||||
@state() private _powerConfig: PowerConfig = {};
|
||||
|
||||
@state() private _power_units?: string[];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _excludeListPower?: string[];
|
||||
|
||||
public async showDialog(
|
||||
params: EnergySettingsGridPowerDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
|
||||
// Initialize from existing source
|
||||
if (params.source?.power_config) {
|
||||
const pc = params.source.power_config;
|
||||
this._powerConfig = { ...pc };
|
||||
if (pc.stat_rate_inverted) {
|
||||
this._sensorType = "inverted";
|
||||
} else if (pc.stat_rate_from || pc.stat_rate_to) {
|
||||
this._sensorType = "two_sensors";
|
||||
} else {
|
||||
this._sensorType = "standard";
|
||||
}
|
||||
} else if (params.source?.stat_rate) {
|
||||
// Legacy format - treat as standard
|
||||
this._sensorType = "standard";
|
||||
this._powerConfig = { stat_rate: params.source.stat_rate };
|
||||
} else {
|
||||
this._sensorType = "standard";
|
||||
this._powerConfig = {};
|
||||
}
|
||||
|
||||
this._power_units = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
|
||||
).units;
|
||||
|
||||
// Build exclude list from all power sources
|
||||
const excludeIds: string[] = [];
|
||||
this._params.grid_source?.power?.forEach((entry) => {
|
||||
if (entry.stat_rate) excludeIds.push(entry.stat_rate);
|
||||
if (entry.power_config) {
|
||||
if (entry.power_config.stat_rate)
|
||||
excludeIds.push(entry.power_config.stat_rate);
|
||||
if (entry.power_config.stat_rate_inverted)
|
||||
excludeIds.push(entry.power_config.stat_rate_inverted);
|
||||
if (entry.power_config.stat_rate_from)
|
||||
excludeIds.push(entry.power_config.stat_rate_from);
|
||||
if (entry.power_config.stat_rate_to)
|
||||
excludeIds.push(entry.power_config.stat_rate_to);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter out current source's IDs
|
||||
const currentIds = [
|
||||
this._powerConfig.stat_rate,
|
||||
this._powerConfig.stat_rate_inverted,
|
||||
this._powerConfig.stat_rate_from,
|
||||
this._powerConfig.stat_rate_to,
|
||||
params.source?.stat_rate,
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
this._excludeListPower = excludeIds.filter(
|
||||
(id) => !currentIds.includes(id)
|
||||
);
|
||||
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._params = undefined;
|
||||
this._powerConfig = {};
|
||||
this._sensorType = "standard";
|
||||
this._error = undefined;
|
||||
this._excludeListPower = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.power_dialog.header"
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : nothing}
|
||||
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.power_dialog.sensor_type"
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.power_dialog.type_standard"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="standard"
|
||||
name="sensorType"
|
||||
.checked=${this._sensorType === "standard"}
|
||||
@change=${this._handleSensorTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.power_dialog.type_inverted"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="inverted"
|
||||
name="sensorType"
|
||||
.checked=${this._sensorType === "inverted"}
|
||||
@change=${this._handleSensorTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.power_dialog.type_two_sensors"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="two_sensors"
|
||||
name="sensorType"
|
||||
.checked=${this._sensorType === "two_sensors"}
|
||||
@change=${this._handleSensorTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
|
||||
${this._sensorType === "standard"
|
||||
? html`
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this._powerConfig.stat_rate}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.power_dialog.power_stat"
|
||||
)}
|
||||
.excludeStatistics=${this._excludeListPower}
|
||||
@value-changed=${this._standardStatisticChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.power_dialog.power_helper",
|
||||
{ unit: this._power_units?.join(", ") || "" }
|
||||
)}
|
||||
autofocus
|
||||
></ha-statistic-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._sensorType === "inverted"
|
||||
? html`
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this._powerConfig.stat_rate_inverted}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.power_dialog.power_stat"
|
||||
)}
|
||||
.excludeStatistics=${this._excludeListPower}
|
||||
@value-changed=${this._invertedStatisticChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.power_dialog.type_inverted_description"
|
||||
)}
|
||||
autofocus
|
||||
></ha-statistic-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._sensorType === "two_sensors"
|
||||
? html`
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this._powerConfig.stat_rate_from}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.power_dialog.power_from_grid"
|
||||
)}
|
||||
.excludeStatistics=${[
|
||||
...(this._excludeListPower || []),
|
||||
this._powerConfig.stat_rate_to,
|
||||
].filter((id): id is string => Boolean(id))}
|
||||
@value-changed=${this._fromStatisticChanged}
|
||||
autofocus
|
||||
></ha-statistic-picker>
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this._powerConfig.stat_rate_to}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.power_dialog.power_to_grid"
|
||||
)}
|
||||
.excludeStatistics=${[
|
||||
...(this._excludeListPower || []),
|
||||
this._powerConfig.stat_rate_from,
|
||||
].filter((id): id is string => Boolean(id))}
|
||||
@value-changed=${this._toStatisticChanged}
|
||||
></ha-statistic-picker>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
slot="secondaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._isValid()}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _isValid(): boolean {
|
||||
switch (this._sensorType) {
|
||||
case "standard":
|
||||
return !!this._powerConfig.stat_rate;
|
||||
case "inverted":
|
||||
return !!this._powerConfig.stat_rate_inverted;
|
||||
case "two_sensors":
|
||||
return (
|
||||
!!this._powerConfig.stat_rate_from && !!this._powerConfig.stat_rate_to
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleSensorTypeChanged(ev: Event) {
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
this._sensorType = input.value as SensorType;
|
||||
// Clear config when switching types
|
||||
this._powerConfig = {};
|
||||
}
|
||||
|
||||
private _standardStatisticChanged(ev: ValueChangedEvent<string>) {
|
||||
this._powerConfig = {
|
||||
stat_rate: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
private _invertedStatisticChanged(ev: ValueChangedEvent<string>) {
|
||||
this._powerConfig = {
|
||||
stat_rate_inverted: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
private _fromStatisticChanged(ev: ValueChangedEvent<string>) {
|
||||
this._powerConfig = {
|
||||
...this._powerConfig,
|
||||
stat_rate_from: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
private _toStatisticChanged(ev: ValueChangedEvent<string>) {
|
||||
this._powerConfig = {
|
||||
...this._powerConfig,
|
||||
stat_rate_to: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
try {
|
||||
const source: GridPowerSourceInput = {
|
||||
power_config: { ...this._powerConfig },
|
||||
};
|
||||
await this._params!.saveCallback(source);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
ha-statistic-picker {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
p {
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-energy-grid-power-settings": DialogEnergyGridPowerSettings;
|
||||
}
|
||||
}
|
||||
662
src/panels/config/energy/dialogs/dialog-energy-grid-settings.ts
Normal file
662
src/panels/config/energy/dialogs/dialog-energy-grid-settings.ts
Normal file
@@ -0,0 +1,662 @@
|
||||
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-entity-picker";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-textfield";
|
||||
import "../../../../components/ha-dialog";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import type {
|
||||
GridSourceTypeEnergyPreference,
|
||||
PowerConfig,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
emptyGridSourceEnergyPreference,
|
||||
energyStatisticHelpUrl,
|
||||
} from "../../../../data/energy";
|
||||
import { isExternalStatistic } from "../../../../data/recorder";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import "./ha-energy-power-config";
|
||||
import {
|
||||
buildPowerExcludeList,
|
||||
getInitialPowerConfig,
|
||||
getPowerTypeFromConfig,
|
||||
type HaEnergyPowerConfig,
|
||||
type PowerType,
|
||||
} from "./ha-energy-power-config";
|
||||
import type { EnergySettingsGridDialogParams } from "./show-dialogs-energy";
|
||||
|
||||
const energyUnitClasses = ["energy"];
|
||||
|
||||
type CostType = "no_cost" | "stat" | "entity" | "number";
|
||||
|
||||
@customElement("dialog-energy-grid-settings")
|
||||
export class DialogEnergyGridSettings
|
||||
extends LitElement
|
||||
implements HassDialog<EnergySettingsGridDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: EnergySettingsGridDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _source?: GridSourceTypeEnergyPreference;
|
||||
|
||||
@state() private _powerType: PowerType = "none";
|
||||
|
||||
@state() private _powerConfig: PowerConfig = {};
|
||||
|
||||
@state() private _importCostType: CostType = "no_cost";
|
||||
|
||||
@state() private _exportCostType: CostType = "no_cost";
|
||||
|
||||
@state() private _energy_units?: string[];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _excludeList?: string[];
|
||||
|
||||
private _excludeListPower?: string[];
|
||||
|
||||
public async showDialog(
|
||||
params: EnergySettingsGridDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._source = params.source
|
||||
? { ...params.source }
|
||||
: emptyGridSourceEnergyPreference();
|
||||
|
||||
// Initialize power type and config from existing source
|
||||
this._powerType = getPowerTypeFromConfig(
|
||||
params.source?.power_config,
|
||||
params.source?.stat_rate
|
||||
);
|
||||
this._powerConfig = getInitialPowerConfig(
|
||||
params.source?.power_config,
|
||||
params.source?.stat_rate
|
||||
);
|
||||
|
||||
// Initialize import cost type
|
||||
if (params.source?.stat_cost) {
|
||||
this._importCostType = "stat";
|
||||
} else if (params.source?.entity_energy_price) {
|
||||
this._importCostType = "entity";
|
||||
} else if (
|
||||
params.source?.number_energy_price !== null &&
|
||||
params.source?.number_energy_price !== undefined
|
||||
) {
|
||||
this._importCostType = "number";
|
||||
} else {
|
||||
this._importCostType = "no_cost";
|
||||
}
|
||||
|
||||
// Initialize export cost type
|
||||
if (params.source?.stat_compensation) {
|
||||
this._exportCostType = "stat";
|
||||
} else if (params.source?.entity_energy_price_export) {
|
||||
this._exportCostType = "entity";
|
||||
} else if (
|
||||
params.source?.number_energy_price_export !== null &&
|
||||
params.source?.number_energy_price_export !== undefined
|
||||
) {
|
||||
this._exportCostType = "number";
|
||||
} else {
|
||||
this._exportCostType = "no_cost";
|
||||
}
|
||||
|
||||
this._energy_units = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
||||
).units;
|
||||
|
||||
// Build energy exclude list
|
||||
const allSources: string[] = [];
|
||||
this._params.grid_sources.forEach((entry) => {
|
||||
if (entry.stat_energy_from) allSources.push(entry.stat_energy_from);
|
||||
if (entry.stat_energy_to) allSources.push(entry.stat_energy_to);
|
||||
});
|
||||
this._excludeList = allSources.filter(
|
||||
(id) =>
|
||||
id !== this._source?.stat_energy_from &&
|
||||
id !== this._source?.stat_energy_to
|
||||
);
|
||||
|
||||
// Build power exclude list using shared helper
|
||||
this._excludeListPower = buildPowerExcludeList(
|
||||
this._params.grid_sources,
|
||||
this._powerConfig,
|
||||
params.source?.stat_rate
|
||||
);
|
||||
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._params = undefined;
|
||||
this._source = undefined;
|
||||
this._powerType = "none";
|
||||
this._powerConfig = {};
|
||||
this._importCostType = "no_cost";
|
||||
this._exportCostType = "no_cost";
|
||||
this._error = undefined;
|
||||
this._excludeList = undefined;
|
||||
this._excludeListPower = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params || !this._source) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const hasExport = !!this._source.stat_energy_to;
|
||||
|
||||
// External statistics (from integrations) cannot use entity/number cost tracking
|
||||
const externalImportSource =
|
||||
this._source.stat_energy_from &&
|
||||
isExternalStatistic(this._source.stat_energy_from);
|
||||
const externalExportSource =
|
||||
this._source.stat_energy_to &&
|
||||
isExternalStatistic(this._source.stat_energy_to);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.header"
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : nothing}
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||
.includeUnitClass=${energyUnitClasses}
|
||||
.value=${this._source.stat_energy_from}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.energy_from_grid"
|
||||
)}
|
||||
.excludeStatistics=${[
|
||||
...(this._excludeList || []),
|
||||
this._source.stat_energy_to,
|
||||
].filter((id): id is string => Boolean(id))}
|
||||
@value-changed=${this._statisticFromChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.energy_from_helper",
|
||||
{ unit: this._energy_units?.join(", ") || "" }
|
||||
)}
|
||||
autofocus
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||
.includeUnitClass=${energyUnitClasses}
|
||||
.value=${this._source.stat_energy_to}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.energy_to_grid"
|
||||
)}
|
||||
.excludeStatistics=${[
|
||||
...(this._excludeList || []),
|
||||
this._source.stat_energy_from,
|
||||
].filter((id): id is string => Boolean(id))}
|
||||
@value-changed=${this._statisticToChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.energy_to_helper",
|
||||
{ unit: this._energy_units?.join(", ") || "" }
|
||||
)}
|
||||
></ha-statistic-picker>
|
||||
|
||||
<p class="section-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.import_cost"
|
||||
)}
|
||||
</p>
|
||||
<p class="section-description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.import_cost_para"
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.no_cost_tracking"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="no_cost"
|
||||
name="importCostType"
|
||||
.checked=${this._importCostType === "no_cost"}
|
||||
@change=${this._handleImportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.cost_stat"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="stat"
|
||||
name="importCostType"
|
||||
.checked=${this._importCostType === "stat"}
|
||||
@change=${this._handleImportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.cost_entity"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="entity"
|
||||
name="importCostType"
|
||||
.checked=${this._importCostType === "entity"}
|
||||
.disabled=${externalImportSource}
|
||||
@change=${this._handleImportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.cost_number"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="number"
|
||||
name="importCostType"
|
||||
.checked=${this._importCostType === "number"}
|
||||
.disabled=${externalImportSource}
|
||||
@change=${this._handleImportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
|
||||
${this._importCostType === "stat"
|
||||
? html`
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._source.stat_cost}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.cost_stat_label"
|
||||
)}
|
||||
@value-changed=${this._statCostChanged}
|
||||
></ha-statistic-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._importCostType === "entity"
|
||||
? html`
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._source.entity_energy_price}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.cost_entity_label"
|
||||
)}
|
||||
include-domains='["sensor", "input_number"]'
|
||||
@value-changed=${this._entityCostChanged}
|
||||
></ha-entity-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._importCostType === "number"
|
||||
? html`
|
||||
<ha-textfield
|
||||
.value=${this._source.number_energy_price ?? ""}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.cost_number_label"
|
||||
)}
|
||||
type="number"
|
||||
step="any"
|
||||
@input=${this._numberCostChanged}
|
||||
.suffix=${`${this.hass.config.currency}/kWh`}
|
||||
></ha-textfield>
|
||||
`
|
||||
: nothing}
|
||||
${hasExport
|
||||
? html`
|
||||
<p class="section-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.export_compensation"
|
||||
)}
|
||||
</p>
|
||||
<p class="section-description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.export_compensation_para"
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.no_compensation_tracking"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="no_cost"
|
||||
name="exportCostType"
|
||||
.checked=${this._exportCostType === "no_cost"}
|
||||
@change=${this._handleExportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.compensation_stat"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="stat"
|
||||
name="exportCostType"
|
||||
.checked=${this._exportCostType === "stat"}
|
||||
@change=${this._handleExportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.compensation_entity"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="entity"
|
||||
name="exportCostType"
|
||||
.checked=${this._exportCostType === "entity"}
|
||||
.disabled=${externalExportSource}
|
||||
@change=${this._handleExportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.compensation_number"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="number"
|
||||
name="exportCostType"
|
||||
.checked=${this._exportCostType === "number"}
|
||||
.disabled=${externalExportSource}
|
||||
@change=${this._handleExportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
|
||||
${this._exportCostType === "stat"
|
||||
? html`
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._source.stat_compensation}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.compensation_stat_label"
|
||||
)}
|
||||
@value-changed=${this._statCompensationChanged}
|
||||
></ha-statistic-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._exportCostType === "entity"
|
||||
? html`
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._source.entity_energy_price_export}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.compensation_entity_label"
|
||||
)}
|
||||
include-domains='["sensor", "input_number"]'
|
||||
@value-changed=${this._entityCompensationChanged}
|
||||
></ha-entity-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._exportCostType === "number"
|
||||
? html`
|
||||
<ha-textfield
|
||||
.value=${this._source.number_energy_price_export ?? ""}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.compensation_number_label"
|
||||
)}
|
||||
type="number"
|
||||
step="any"
|
||||
@input=${this._numberCompensationChanged}
|
||||
.suffix=${`${this.hass.config.currency}/kWh`}
|
||||
></ha-textfield>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-energy-power-config
|
||||
.hass=${this.hass}
|
||||
.powerType=${this._powerType}
|
||||
.powerConfig=${this._powerConfig}
|
||||
.excludeList=${this._excludeListPower}
|
||||
localizeBaseKey="ui.panel.config.energy.grid.dialog"
|
||||
@power-config-changed=${this._handlePowerConfigChanged}
|
||||
></ha-energy-power-config>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
slot="secondaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._isValid()}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _isValid(): boolean {
|
||||
// Grid must have at least one of: import, export, or power
|
||||
const hasImport = !!this._source?.stat_energy_from;
|
||||
const hasExport = !!this._source?.stat_energy_to;
|
||||
const hasPower = this._powerType !== "none";
|
||||
|
||||
if (!hasImport && !hasExport && !hasPower) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check power config validity (if power is configured)
|
||||
if (hasPower) {
|
||||
const powerConfigEl = this.shadowRoot?.querySelector(
|
||||
"ha-energy-power-config"
|
||||
) as HaEnergyPowerConfig | null;
|
||||
if (powerConfigEl && !powerConfigEl.isValid()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _statisticFromChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
|
||||
// Reset cost type if switching to external statistic with incompatible cost type
|
||||
if (
|
||||
ev.detail.value &&
|
||||
isExternalStatistic(ev.detail.value) &&
|
||||
(this._importCostType === "entity" || this._importCostType === "number")
|
||||
) {
|
||||
this._importCostType = "no_cost";
|
||||
this._source = {
|
||||
...this._source!,
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private _statisticToChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = {
|
||||
...this._source!,
|
||||
stat_energy_to: ev.detail.value || null,
|
||||
};
|
||||
// Clear export cost if export is removed
|
||||
if (!ev.detail.value) {
|
||||
this._exportCostType = "no_cost";
|
||||
this._source = {
|
||||
...this._source!,
|
||||
stat_compensation: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
};
|
||||
} else if (
|
||||
// Reset cost type if switching to external statistic with incompatible cost type
|
||||
isExternalStatistic(ev.detail.value) &&
|
||||
(this._exportCostType === "entity" || this._exportCostType === "number")
|
||||
) {
|
||||
this._exportCostType = "no_cost";
|
||||
this._source = {
|
||||
...this._source!,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private _handleImportCostTypeChanged(ev: Event) {
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
this._importCostType = input.value as CostType;
|
||||
// Clear other cost fields when switching types
|
||||
this._source = {
|
||||
...this._source!,
|
||||
stat_cost: null,
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
};
|
||||
}
|
||||
|
||||
private _handleExportCostTypeChanged(ev: Event) {
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
this._exportCostType = input.value as CostType;
|
||||
// Clear other cost fields when switching types
|
||||
this._source = {
|
||||
...this._source!,
|
||||
stat_compensation: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
};
|
||||
}
|
||||
|
||||
private _statCostChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = { ...this._source!, stat_cost: ev.detail.value || null };
|
||||
}
|
||||
|
||||
private _entityCostChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = {
|
||||
...this._source!,
|
||||
entity_energy_price: ev.detail.value || null,
|
||||
};
|
||||
}
|
||||
|
||||
private _numberCostChanged(ev: Event) {
|
||||
const input = ev.currentTarget as HTMLInputElement;
|
||||
const value = input.value ? parseFloat(input.value) : null;
|
||||
this._source = { ...this._source!, number_energy_price: value };
|
||||
}
|
||||
|
||||
private _statCompensationChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = {
|
||||
...this._source!,
|
||||
stat_compensation: ev.detail.value || null,
|
||||
};
|
||||
}
|
||||
|
||||
private _entityCompensationChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = {
|
||||
...this._source!,
|
||||
entity_energy_price_export: ev.detail.value || null,
|
||||
};
|
||||
}
|
||||
|
||||
private _numberCompensationChanged(ev: Event) {
|
||||
const input = ev.currentTarget as HTMLInputElement;
|
||||
const value = input.value ? parseFloat(input.value) : null;
|
||||
this._source = { ...this._source!, number_energy_price_export: value };
|
||||
}
|
||||
|
||||
private _handlePowerConfigChanged(
|
||||
ev: CustomEvent<{ powerType: PowerType; powerConfig: PowerConfig }>
|
||||
) {
|
||||
this._powerType = ev.detail.powerType;
|
||||
this._powerConfig = ev.detail.powerConfig;
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
try {
|
||||
const source: GridSourceTypeEnergyPreference = {
|
||||
type: "grid",
|
||||
stat_energy_from: this._source!.stat_energy_from,
|
||||
stat_energy_to: this._source!.stat_energy_to,
|
||||
stat_cost: this._source!.stat_cost,
|
||||
stat_compensation: this._source!.stat_compensation,
|
||||
entity_energy_price: this._source!.entity_energy_price,
|
||||
number_energy_price: this._source!.number_energy_price,
|
||||
entity_energy_price_export: this._source!.entity_energy_price_export,
|
||||
number_energy_price_export: this._source!.number_energy_price_export,
|
||||
cost_adjustment_day: this._source!.cost_adjustment_day,
|
||||
};
|
||||
|
||||
// Only include power_config if a power type is selected
|
||||
if (this._powerType !== "none") {
|
||||
source.power_config = { ...this._powerConfig };
|
||||
}
|
||||
|
||||
await this._params!.saveCallback(source);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-statistic-picker,
|
||||
ha-entity-picker,
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
ha-statistic-picker:last-of-type,
|
||||
ha-entity-picker:last-of-type,
|
||||
ha-textfield:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
.section-label {
|
||||
margin-top: var(--ha-space-4);
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
.section-description {
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 0.875em;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-energy-grid-settings": DialogEnergyGridSettings;
|
||||
}
|
||||
}
|
||||
351
src/panels/config/energy/dialogs/ha-energy-power-config.ts
Normal file
351
src/panels/config/energy/dialogs/ha-energy-power-config.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../../../../common/translations/localize";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import type { PowerConfig } from "../../../../data/energy";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
|
||||
export type PowerType = "none" | "standard" | "inverted" | "two_sensors";
|
||||
|
||||
const powerUnitClasses = ["power"];
|
||||
|
||||
/**
|
||||
* Extracts the power type from a PowerConfig object.
|
||||
*/
|
||||
export function getPowerTypeFromConfig(
|
||||
powerConfig?: PowerConfig,
|
||||
statRate?: string
|
||||
): PowerType {
|
||||
if (powerConfig) {
|
||||
if (powerConfig.stat_rate_inverted) {
|
||||
return "inverted";
|
||||
}
|
||||
if (powerConfig.stat_rate_from || powerConfig.stat_rate_to) {
|
||||
return "two_sensors";
|
||||
}
|
||||
if (powerConfig.stat_rate) {
|
||||
return "standard";
|
||||
}
|
||||
} else if (statRate) {
|
||||
// Legacy format - treat as standard
|
||||
return "standard";
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an initial PowerConfig from existing config or legacy stat_rate.
|
||||
*/
|
||||
export function getInitialPowerConfig(
|
||||
powerConfig?: PowerConfig,
|
||||
statRate?: string
|
||||
): PowerConfig {
|
||||
if (powerConfig) {
|
||||
return { ...powerConfig };
|
||||
}
|
||||
if (statRate) {
|
||||
return { stat_rate: statRate };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an exclude list for power statistics from existing sources.
|
||||
*/
|
||||
export function buildPowerExcludeList(
|
||||
sources: { stat_rate?: string; power_config?: PowerConfig }[],
|
||||
currentPowerConfig: PowerConfig,
|
||||
currentStatRate?: string
|
||||
): string[] {
|
||||
const powerIds: string[] = [];
|
||||
|
||||
sources.forEach((entry) => {
|
||||
if (entry.stat_rate) powerIds.push(entry.stat_rate);
|
||||
if (entry.power_config) {
|
||||
if (entry.power_config.stat_rate)
|
||||
powerIds.push(entry.power_config.stat_rate);
|
||||
if (entry.power_config.stat_rate_inverted)
|
||||
powerIds.push(entry.power_config.stat_rate_inverted);
|
||||
if (entry.power_config.stat_rate_from)
|
||||
powerIds.push(entry.power_config.stat_rate_from);
|
||||
if (entry.power_config.stat_rate_to)
|
||||
powerIds.push(entry.power_config.stat_rate_to);
|
||||
}
|
||||
});
|
||||
|
||||
const currentPowerIds = [
|
||||
currentPowerConfig.stat_rate,
|
||||
currentPowerConfig.stat_rate_inverted,
|
||||
currentPowerConfig.stat_rate_from,
|
||||
currentPowerConfig.stat_rate_to,
|
||||
currentStatRate,
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
return powerIds.filter((id) => !currentPowerIds.includes(id));
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"power-config-changed": { powerType: PowerType; powerConfig: PowerConfig };
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-energy-power-config")
|
||||
export class HaEnergyPowerConfig extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public powerType: PowerType = "none";
|
||||
|
||||
@property({ attribute: false }) public powerConfig: PowerConfig = {};
|
||||
|
||||
@property({ attribute: false }) public excludeList?: string[];
|
||||
|
||||
/**
|
||||
* Base key for localization lookups.
|
||||
* Should include keys for: sensor_type, type_none, type_standard, type_inverted,
|
||||
* type_two_sensors, power, power_helper, type_inverted_description, power_from, power_to
|
||||
*/
|
||||
@property({ attribute: false }) public localizeBaseKey =
|
||||
"ui.panel.config.energy.battery.dialog";
|
||||
|
||||
@state() private _powerUnits?: string[];
|
||||
|
||||
protected async willUpdate(changedProps: PropertyValues): Promise<void> {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (changedProps.has("hass") && !this._powerUnits) {
|
||||
this._powerUnits = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
|
||||
).units;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<p class="power-section-label">
|
||||
${this.hass.localize(
|
||||
`${this.localizeBaseKey}.sensor_type` as LocalizeKeys
|
||||
)}
|
||||
</p>
|
||||
<p class="power-section-description">
|
||||
${this.hass.localize(
|
||||
`${this.localizeBaseKey}.sensor_type_para` as LocalizeKeys
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
`${this.localizeBaseKey}.type_none` as LocalizeKeys
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="none"
|
||||
name="powerType"
|
||||
.checked=${this.powerType === "none"}
|
||||
@change=${this._handlePowerTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
`${this.localizeBaseKey}.type_standard` as LocalizeKeys
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="standard"
|
||||
name="powerType"
|
||||
.checked=${this.powerType === "standard"}
|
||||
@change=${this._handlePowerTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
`${this.localizeBaseKey}.type_inverted` as LocalizeKeys
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="inverted"
|
||||
name="powerType"
|
||||
.checked=${this.powerType === "inverted"}
|
||||
@change=${this._handlePowerTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
`${this.localizeBaseKey}.type_two_sensors` as LocalizeKeys
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="two_sensors"
|
||||
name="powerType"
|
||||
.checked=${this.powerType === "two_sensors"}
|
||||
@change=${this._handlePowerTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
|
||||
${this.powerType === "standard"
|
||||
? html`
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this.powerConfig.stat_rate}
|
||||
.label=${this.hass.localize(
|
||||
`${this.localizeBaseKey}.power` as LocalizeKeys
|
||||
)}
|
||||
.excludeStatistics=${this.excludeList}
|
||||
@value-changed=${this._standardPowerChanged}
|
||||
.helper=${this.hass.localize(
|
||||
`${this.localizeBaseKey}.power_helper` as LocalizeKeys,
|
||||
{ unit: this._powerUnits?.join(", ") || "" }
|
||||
)}
|
||||
></ha-statistic-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this.powerType === "inverted"
|
||||
? html`
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this.powerConfig.stat_rate_inverted}
|
||||
.label=${this.hass.localize(
|
||||
`${this.localizeBaseKey}.power` as LocalizeKeys
|
||||
)}
|
||||
.excludeStatistics=${this.excludeList}
|
||||
@value-changed=${this._invertedPowerChanged}
|
||||
.helper=${this.hass.localize(
|
||||
`${this.localizeBaseKey}.type_inverted_description` as LocalizeKeys
|
||||
)}
|
||||
></ha-statistic-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this.powerType === "two_sensors"
|
||||
? html`
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this.powerConfig.stat_rate_from}
|
||||
.label=${this.hass.localize(
|
||||
`${this.localizeBaseKey}.power_from` as LocalizeKeys
|
||||
)}
|
||||
.excludeStatistics=${[
|
||||
...(this.excludeList || []),
|
||||
this.powerConfig.stat_rate_to,
|
||||
].filter((id): id is string => Boolean(id))}
|
||||
@value-changed=${this._fromPowerChanged}
|
||||
></ha-statistic-picker>
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this.powerConfig.stat_rate_to}
|
||||
.label=${this.hass.localize(
|
||||
`${this.localizeBaseKey}.power_to` as LocalizeKeys
|
||||
)}
|
||||
.excludeStatistics=${[
|
||||
...(this.excludeList || []),
|
||||
this.powerConfig.stat_rate_from,
|
||||
].filter((id): id is string => Boolean(id))}
|
||||
@value-changed=${this._toPowerChanged}
|
||||
></ha-statistic-picker>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handlePowerTypeChanged(ev: Event) {
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
const newPowerType = input.value as PowerType;
|
||||
// Clear power config when switching types
|
||||
fireEvent(this, "power-config-changed", {
|
||||
powerType: newPowerType,
|
||||
powerConfig: {},
|
||||
});
|
||||
}
|
||||
|
||||
private _standardPowerChanged(ev: ValueChangedEvent<string>) {
|
||||
fireEvent(this, "power-config-changed", {
|
||||
powerType: this.powerType,
|
||||
powerConfig: { stat_rate: ev.detail.value },
|
||||
});
|
||||
}
|
||||
|
||||
private _invertedPowerChanged(ev: ValueChangedEvent<string>) {
|
||||
fireEvent(this, "power-config-changed", {
|
||||
powerType: this.powerType,
|
||||
powerConfig: { stat_rate_inverted: ev.detail.value },
|
||||
});
|
||||
}
|
||||
|
||||
private _fromPowerChanged(ev: ValueChangedEvent<string>) {
|
||||
fireEvent(this, "power-config-changed", {
|
||||
powerType: this.powerType,
|
||||
powerConfig: {
|
||||
...this.powerConfig,
|
||||
stat_rate_from: ev.detail.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _toPowerChanged(ev: ValueChangedEvent<string>) {
|
||||
fireEvent(this, "power-config-changed", {
|
||||
powerType: this.powerType,
|
||||
powerConfig: {
|
||||
...this.powerConfig,
|
||||
stat_rate_to: ev.detail.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the power config is complete for the selected type.
|
||||
*/
|
||||
public isValid(): boolean {
|
||||
switch (this.powerType) {
|
||||
case "none":
|
||||
return true;
|
||||
case "standard":
|
||||
return !!this.powerConfig.stat_rate;
|
||||
case "inverted":
|
||||
return !!this.powerConfig.stat_rate_inverted;
|
||||
case "two_sensors":
|
||||
return (
|
||||
!!this.powerConfig.stat_rate_from && !!this.powerConfig.stat_rate_to
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static readonly styles: CSSResultGroup = css`
|
||||
ha-statistic-picker {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
ha-statistic-picker:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
.power-section-label {
|
||||
margin-top: var(--ha-space-4);
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
.power-section-description {
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 0.875em;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-energy-power-config": HaEnergyPowerConfig;
|
||||
}
|
||||
}
|
||||
@@ -4,49 +4,17 @@ import type {
|
||||
DeviceConsumptionEnergyPreference,
|
||||
EnergyGasUnitClass,
|
||||
EnergyInfo,
|
||||
FlowFromGridSourceEnergyPreference,
|
||||
FlowToGridSourceEnergyPreference,
|
||||
GasSourceTypeEnergyPreference,
|
||||
GridPowerSourceEnergyPreference,
|
||||
GridPowerSourceInput,
|
||||
GridSourceTypeEnergyPreference,
|
||||
SolarSourceTypeEnergyPreference,
|
||||
WaterSourceTypeEnergyPreference,
|
||||
} from "../../../../data/energy";
|
||||
import type { StatisticsMetaData } from "../../../../data/recorder";
|
||||
|
||||
export interface EnergySettingsGridFlowDialogParams {
|
||||
source?:
|
||||
| FlowFromGridSourceEnergyPreference
|
||||
| FlowToGridSourceEnergyPreference;
|
||||
metadata?: StatisticsMetaData;
|
||||
direction: "from" | "to";
|
||||
grid_source?: GridSourceTypeEnergyPreference;
|
||||
saveCallback: (
|
||||
source:
|
||||
| FlowFromGridSourceEnergyPreference
|
||||
| FlowToGridSourceEnergyPreference
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface EnergySettingsGridFlowFromDialogParams {
|
||||
source?: FlowFromGridSourceEnergyPreference;
|
||||
metadata?: StatisticsMetaData;
|
||||
grid_source?: GridSourceTypeEnergyPreference;
|
||||
saveCallback: (source: FlowFromGridSourceEnergyPreference) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface EnergySettingsGridFlowToDialogParams {
|
||||
source?: FlowToGridSourceEnergyPreference;
|
||||
metadata?: StatisticsMetaData;
|
||||
grid_source?: GridSourceTypeEnergyPreference;
|
||||
saveCallback: (source: FlowToGridSourceEnergyPreference) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface EnergySettingsGridPowerDialogParams {
|
||||
source?: GridPowerSourceEnergyPreference;
|
||||
grid_source?: GridSourceTypeEnergyPreference;
|
||||
saveCallback: (source: GridPowerSourceInput) => Promise<void>;
|
||||
export interface EnergySettingsGridDialogParams {
|
||||
source?: GridSourceTypeEnergyPreference;
|
||||
grid_sources: GridSourceTypeEnergyPreference[];
|
||||
saveCallback: (source: GridSourceTypeEnergyPreference) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface EnergySettingsSolarDialogParams {
|
||||
@@ -146,28 +114,6 @@ export const showEnergySettingsWaterDialog = (
|
||||
});
|
||||
};
|
||||
|
||||
export const showEnergySettingsGridFlowFromDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: EnergySettingsGridFlowFromDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-energy-grid-flow-settings",
|
||||
dialogImport: () => import("./dialog-energy-grid-flow-settings"),
|
||||
dialogParams: { ...dialogParams, direction: "from" },
|
||||
});
|
||||
};
|
||||
|
||||
export const showEnergySettingsGridFlowToDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: EnergySettingsGridFlowToDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-energy-grid-flow-settings",
|
||||
dialogImport: () => import("./dialog-energy-grid-flow-settings"),
|
||||
dialogParams: { ...dialogParams, direction: "to" },
|
||||
});
|
||||
};
|
||||
|
||||
export const showEnergySettingsDeviceWaterDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: EnergySettingsDeviceWaterDialogParams
|
||||
@@ -179,13 +125,13 @@ export const showEnergySettingsDeviceWaterDialog = (
|
||||
});
|
||||
};
|
||||
|
||||
export const showEnergySettingsGridPowerDialog = (
|
||||
export const showEnergySettingsGridDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: EnergySettingsGridPowerDialogParams
|
||||
dialogParams: EnergySettingsGridDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-energy-grid-power-settings",
|
||||
dialogImport: () => import("./dialog-energy-grid-power-settings"),
|
||||
dialogTag: "dialog-energy-grid-settings",
|
||||
dialogImport: () => import("./dialog-energy-grid-settings"),
|
||||
dialogParams: dialogParams,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -290,12 +290,14 @@ class PanelEnergy extends LitElement {
|
||||
["grid", "solar", "battery"].includes(source.type)
|
||||
);
|
||||
|
||||
const hasPowerSource = this._prefs.energy_sources.some(
|
||||
(source) =>
|
||||
(source.type === "solar" && source.stat_rate) ||
|
||||
(source.type === "battery" && source.stat_rate) ||
|
||||
(source.type === "grid" && source.power?.length)
|
||||
);
|
||||
const hasPowerSource = this._prefs.energy_sources.some((source) => {
|
||||
if (source.type === "solar" && source.stat_rate) return true;
|
||||
if (source.type === "battery" && source.stat_rate) return true;
|
||||
if (source.type === "grid") {
|
||||
return !!source.stat_rate || !!source.power_config;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const hasDevicePower = this._prefs.device_consumption.some(
|
||||
(device) => device.stat_rate
|
||||
@@ -435,26 +437,25 @@ class PanelEnergy extends LitElement {
|
||||
energy_sources
|
||||
.filter((s) => s.type === "grid")
|
||||
.forEach((source) => {
|
||||
source = source as GridSourceTypeEnergyPreference;
|
||||
source.flow_from.forEach((flowFrom) => {
|
||||
const statId = flowFrom.stat_energy_from;
|
||||
grid_consumptions.push(statId);
|
||||
const costId =
|
||||
flowFrom.stat_cost || energyData.state.info.cost_sensors[statId];
|
||||
if (costId) {
|
||||
grid_consumptions_cost.push(costId);
|
||||
const gridSource = source as GridSourceTypeEnergyPreference;
|
||||
if (gridSource.stat_energy_from) {
|
||||
grid_consumptions.push(gridSource.stat_energy_from);
|
||||
const importCostId =
|
||||
gridSource.stat_cost ||
|
||||
energyData.state.info.cost_sensors[gridSource.stat_energy_from];
|
||||
if (importCostId) {
|
||||
grid_consumptions_cost.push(importCostId);
|
||||
}
|
||||
});
|
||||
source.flow_to.forEach((flowTo) => {
|
||||
const statId = flowTo.stat_energy_to;
|
||||
grid_productions.push(statId);
|
||||
const costId =
|
||||
flowTo.stat_compensation ||
|
||||
energyData.state.info.cost_sensors[statId];
|
||||
if (costId) {
|
||||
grid_productions_cost.push(costId);
|
||||
}
|
||||
if (gridSource.stat_energy_to) {
|
||||
grid_productions.push(gridSource.stat_energy_to);
|
||||
const exportCostId =
|
||||
gridSource.stat_compensation ||
|
||||
energyData.state.info.cost_sensors[gridSource.stat_energy_to];
|
||||
if (exportCostId) {
|
||||
grid_productions_cost.push(exportCostId);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
printCategory(
|
||||
|
||||
@@ -41,10 +41,10 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
|
||||
}
|
||||
|
||||
const hasGrid = prefs.energy_sources.find(
|
||||
(source) =>
|
||||
(source): source is GridSourceTypeEnergyPreference =>
|
||||
source.type === "grid" &&
|
||||
(source.flow_from?.length || source.flow_to?.length)
|
||||
) as GridSourceTypeEnergyPreference;
|
||||
(!!source.stat_energy_from || !!source.stat_energy_to)
|
||||
);
|
||||
const hasGas = prefs.energy_sources.some((source) => source.type === "gas");
|
||||
const hasBattery = prefs.energy_sources.some(
|
||||
(source) => source.type === "battery"
|
||||
@@ -56,12 +56,14 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
|
||||
(source) => source.type === "water"
|
||||
);
|
||||
const hasWaterDevices = prefs.device_consumption_water?.length;
|
||||
const hasPowerSources = prefs.energy_sources.find(
|
||||
(source) =>
|
||||
(source.type === "solar" && source.stat_rate) ||
|
||||
(source.type === "battery" && source.stat_rate) ||
|
||||
(source.type === "grid" && source.power?.length)
|
||||
);
|
||||
const hasPowerSources = prefs.energy_sources.find((source) => {
|
||||
if (source.type === "solar" && source.stat_rate) return true;
|
||||
if (source.type === "battery" && source.stat_rate) return true;
|
||||
if (source.type === "grid") {
|
||||
return !!source.stat_rate || !!source.power_config;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasGrid || hasBattery || hasSolar) {
|
||||
view.sections!.push({
|
||||
|
||||
@@ -39,11 +39,11 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
view.type = "sidebar";
|
||||
|
||||
const hasGrid = prefs.energy_sources.find(
|
||||
(source) =>
|
||||
(source): source is GridSourceTypeEnergyPreference =>
|
||||
source.type === "grid" &&
|
||||
(source.flow_from?.length || source.flow_to?.length)
|
||||
) as GridSourceTypeEnergyPreference;
|
||||
const hasReturn = hasGrid && hasGrid.flow_to.length;
|
||||
(!!source.stat_energy_from || !!source.stat_energy_to)
|
||||
);
|
||||
const hasReturn = hasGrid && !!hasGrid.stat_energy_to;
|
||||
const hasSolar = prefs.energy_sources.some(
|
||||
(source) => source.type === "solar"
|
||||
);
|
||||
|
||||
@@ -28,12 +28,14 @@ export class PowerViewStrategy extends ReactiveElement {
|
||||
await energyCollection.refresh();
|
||||
const prefs = energyCollection.prefs;
|
||||
|
||||
const hasPowerSources = prefs?.energy_sources.some(
|
||||
(source) =>
|
||||
(source.type === "solar" && source.stat_rate) ||
|
||||
(source.type === "battery" && source.stat_rate) ||
|
||||
(source.type === "grid" && source.power?.length)
|
||||
);
|
||||
const hasPowerSources = prefs?.energy_sources.some((source) => {
|
||||
if (source.type === "solar" && source.stat_rate) return true;
|
||||
if (source.type === "battery" && source.stat_rate) return true;
|
||||
if (source.type === "grid") {
|
||||
return !!source.stat_rate || !!source.power_config;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const hasPowerDevices = prefs?.device_consumption.some(
|
||||
(device) => device.stat_rate
|
||||
);
|
||||
|
||||
@@ -112,12 +112,13 @@ class HuiEnergyDistrubutionCard
|
||||
const types = energySourcesByType(prefs);
|
||||
|
||||
const hasGrid =
|
||||
!!types.grid?.[0].flow_from.length || !!types.grid?.[0].flow_to.length;
|
||||
!!types.grid?.[0] &&
|
||||
(!!types.grid[0].stat_energy_from || !!types.grid[0].stat_energy_to);
|
||||
const hasSolarProduction = types.solar !== undefined;
|
||||
const hasBattery = types.battery !== undefined;
|
||||
const hasGas = types.gas !== undefined;
|
||||
const hasWater = types.water !== undefined;
|
||||
const hasReturnToGrid = !!types.grid?.[0].flow_to.length;
|
||||
const hasReturnToGrid = !!types.grid?.[0] && !!types.grid[0].stat_energy_to;
|
||||
|
||||
const { summedData, compareSummedData: _ } = getSummedData(this._data);
|
||||
const { consumption, compareConsumption: __ } = computeConsumptionData(
|
||||
|
||||
@@ -211,7 +211,7 @@ class HuiEnergySankeyCard
|
||||
}
|
||||
|
||||
// Add grid return if available
|
||||
if (types.grid && types.grid[0].flow_to) {
|
||||
if (types.grid && types.grid[0].stat_energy_to) {
|
||||
const totalToGrid = summedData.total.to_grid ?? 0;
|
||||
|
||||
nodes.push({
|
||||
|
||||
@@ -278,17 +278,19 @@ export class HuiEnergySourcesTableCard
|
||||
|
||||
const computedStyles = getComputedStyle(this);
|
||||
|
||||
// Check if any source has cost configuration
|
||||
const gridHasCosts = types.grid?.some(
|
||||
(source) =>
|
||||
source.stat_cost ||
|
||||
source.entity_energy_price ||
|
||||
source.number_energy_price ||
|
||||
source.stat_compensation ||
|
||||
source.entity_energy_price_export ||
|
||||
source.number_energy_price_export
|
||||
);
|
||||
|
||||
const showCosts = !!(
|
||||
types.grid?.[0].flow_from.some(
|
||||
(flow) =>
|
||||
flow.stat_cost || flow.entity_energy_price || flow.number_energy_price
|
||||
) ||
|
||||
types.grid?.[0].flow_to.some(
|
||||
(flow) =>
|
||||
flow.stat_compensation ||
|
||||
flow.entity_energy_price ||
|
||||
flow.number_energy_price
|
||||
) ||
|
||||
gridHasCosts ||
|
||||
types.gas?.some(
|
||||
(flow) =>
|
||||
flow.stat_cost || flow.entity_energy_price || flow.number_energy_price
|
||||
@@ -578,103 +580,95 @@ export class HuiEnergySourcesTableCard
|
||||
: undefined
|
||||
)
|
||||
: ""}
|
||||
${types.grid?.map(
|
||||
(source) =>
|
||||
html`${source.flow_from.map((flow, idx) => {
|
||||
const cost_stat =
|
||||
flow.stat_cost ||
|
||||
this._data!.info.cost_sensors[flow.stat_energy_from];
|
||||
const {
|
||||
hasData,
|
||||
energy,
|
||||
energyCompare,
|
||||
cost,
|
||||
costCompare,
|
||||
} = _extractStatData(
|
||||
flow.stat_energy_from,
|
||||
${types.grid?.map((source, idx) => {
|
||||
const importResult = (() => {
|
||||
if (!source.stat_energy_from) return nothing;
|
||||
|
||||
const cost_stat =
|
||||
source.stat_cost ||
|
||||
this._data!.info.cost_sensors[source.stat_energy_from];
|
||||
const { hasData, energy, energyCompare, cost, costCompare } =
|
||||
_extractStatData(
|
||||
source.stat_energy_from,
|
||||
cost_stat || null
|
||||
);
|
||||
|
||||
if (!hasData && !cost && !costCompare) {
|
||||
return nothing;
|
||||
}
|
||||
if (!hasData && !cost && !costCompare) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
totalGrid += energy;
|
||||
totalGridCompare += energyCompare;
|
||||
totalGrid += energy;
|
||||
totalGridCompare += energyCompare;
|
||||
|
||||
if (cost_stat) {
|
||||
hasGridCost = true;
|
||||
totalGridCost += cost;
|
||||
totalGridCostCompare += costCompare;
|
||||
}
|
||||
if (cost_stat) {
|
||||
hasGridCost = true;
|
||||
totalGridCost += cost;
|
||||
totalGridCostCompare += costCompare;
|
||||
}
|
||||
|
||||
if (showOnlyTotals) {
|
||||
return nothing;
|
||||
}
|
||||
if (showOnlyTotals) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return this._renderRow(
|
||||
computedStyles,
|
||||
"grid_consumption",
|
||||
flow.stat_energy_from,
|
||||
idx,
|
||||
energy,
|
||||
energyCompare,
|
||||
"kWh",
|
||||
cost,
|
||||
costCompare,
|
||||
showCosts,
|
||||
compare
|
||||
);
|
||||
})}
|
||||
${source.flow_to.map((flow, idx) => {
|
||||
const cost_stat =
|
||||
flow.stat_compensation ||
|
||||
this._data!.info.cost_sensors[flow.stat_energy_to];
|
||||
const {
|
||||
hasData,
|
||||
energy,
|
||||
energyCompare,
|
||||
cost,
|
||||
costCompare,
|
||||
} = _extractStatData(
|
||||
flow.stat_energy_to,
|
||||
cost_stat || null
|
||||
);
|
||||
return this._renderRow(
|
||||
computedStyles,
|
||||
"grid_consumption",
|
||||
source.stat_energy_from,
|
||||
idx,
|
||||
energy,
|
||||
energyCompare,
|
||||
"kWh",
|
||||
cost,
|
||||
costCompare,
|
||||
showCosts,
|
||||
compare
|
||||
);
|
||||
})();
|
||||
|
||||
if (!hasData && !cost && !costCompare) {
|
||||
return nothing;
|
||||
}
|
||||
totalGrid -= energy;
|
||||
totalGridCompare -= energyCompare;
|
||||
const exportResult = (() => {
|
||||
if (!source.stat_energy_to) return nothing;
|
||||
|
||||
if (cost_stat !== null) {
|
||||
hasGridCost = true;
|
||||
totalGridCost -= cost;
|
||||
totalGridCostCompare -= costCompare;
|
||||
}
|
||||
const cost_stat =
|
||||
source.stat_compensation ||
|
||||
this._data!.info.cost_sensors[source.stat_energy_to];
|
||||
const { hasData, energy, energyCompare, cost, costCompare } =
|
||||
_extractStatData(source.stat_energy_to, cost_stat || null);
|
||||
|
||||
if (showOnlyTotals) {
|
||||
return nothing;
|
||||
}
|
||||
if (!hasData && !cost && !costCompare) {
|
||||
return nothing;
|
||||
}
|
||||
totalGrid -= energy;
|
||||
totalGridCompare -= energyCompare;
|
||||
|
||||
return this._renderRow(
|
||||
computedStyles,
|
||||
"grid_return",
|
||||
flow.stat_energy_to,
|
||||
idx,
|
||||
-energy,
|
||||
-energyCompare,
|
||||
"kWh",
|
||||
-cost,
|
||||
-costCompare,
|
||||
showCosts,
|
||||
compare
|
||||
);
|
||||
})}`
|
||||
)}
|
||||
if (cost_stat) {
|
||||
hasGridCost = true;
|
||||
totalGridCost -= cost;
|
||||
totalGridCostCompare -= costCompare;
|
||||
}
|
||||
|
||||
if (showOnlyTotals) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return this._renderRow(
|
||||
computedStyles,
|
||||
"grid_return",
|
||||
source.stat_energy_to,
|
||||
idx,
|
||||
-energy,
|
||||
-energyCompare,
|
||||
"kWh",
|
||||
-cost,
|
||||
-costCompare,
|
||||
showCosts,
|
||||
compare
|
||||
);
|
||||
})();
|
||||
|
||||
return html`${importResult}${exportResult}`;
|
||||
})}
|
||||
${types.grid &&
|
||||
(types.grid?.[0].flow_from?.length ||
|
||||
types.grid?.[0].flow_to?.length)
|
||||
types.grid.some((s) => !!s.stat_energy_from || !!s.stat_energy_to)
|
||||
? this._renderTotalRow(
|
||||
this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_sources_table.grid_total"
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
EnergyData,
|
||||
EnergySumData,
|
||||
EnergyConsumptionData,
|
||||
GridSourceTypeEnergyPreference,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
computeConsumptionData,
|
||||
@@ -248,19 +249,19 @@ export class HuiEnergyUsageGraphCard
|
||||
continue;
|
||||
}
|
||||
|
||||
// grid source
|
||||
for (const flowFrom of source.flow_from) {
|
||||
const gridSource = source as GridSourceTypeEnergyPreference;
|
||||
if (gridSource.stat_energy_from) {
|
||||
if (statIds.from_grid) {
|
||||
statIds.from_grid.push(flowFrom.stat_energy_from);
|
||||
statIds.from_grid.push(gridSource.stat_energy_from);
|
||||
} else {
|
||||
statIds.from_grid = [flowFrom.stat_energy_from];
|
||||
statIds.from_grid = [gridSource.stat_energy_from];
|
||||
}
|
||||
}
|
||||
for (const flowTo of source.flow_to) {
|
||||
if (gridSource.stat_energy_to) {
|
||||
if (statIds.to_grid) {
|
||||
statIds.to_grid.push(flowTo.stat_energy_to);
|
||||
statIds.to_grid.push(gridSource.stat_energy_to);
|
||||
} else {
|
||||
statIds.to_grid = [flowTo.stat_energy_to];
|
||||
statIds.to_grid = [gridSource.stat_energy_to];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,17 +599,15 @@ class HuiPowerSankeyCard
|
||||
|
||||
// Collect grid power (positive = import, negative = export)
|
||||
prefs.energy_sources
|
||||
.filter((source) => source.type === "grid" && source.power)
|
||||
.filter((source) => source.type === "grid")
|
||||
.forEach((source) => {
|
||||
if (source.type === "grid" && source.power) {
|
||||
source.power.forEach((powerSource) => {
|
||||
const value = this._getCurrentPower(powerSource.stat_rate);
|
||||
if (value > 0) {
|
||||
from_grid += value;
|
||||
} else if (value < 0) {
|
||||
to_grid += Math.abs(value);
|
||||
}
|
||||
});
|
||||
if (source.type === "grid" && source.stat_rate) {
|
||||
const value = this._getCurrentPower(source.stat_rate);
|
||||
if (value > 0) {
|
||||
from_grid += value;
|
||||
} else if (value < 0) {
|
||||
to_grid += Math.abs(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -189,8 +189,10 @@ export class HuiPowerSourcesGraphCard
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.type === "grid" && source.power) {
|
||||
statIds.grid.stats.push(...source.power.map((p) => p.stat_rate));
|
||||
if (source.type === "grid") {
|
||||
if (source.stat_rate) {
|
||||
statIds.grid.stats.push(source.stat_rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
const commonSeriesOptions: LineSeriesOption = {
|
||||
|
||||
@@ -644,7 +644,7 @@ export const generateDefaultViewConfig = (
|
||||
(source) => source.type === "grid"
|
||||
) as GridSourceTypeEnergyPreference | undefined;
|
||||
|
||||
if (grid && grid.flow_from.length > 0) {
|
||||
if (grid && grid.stat_energy_from) {
|
||||
energyCard = {
|
||||
title: localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.title_today"
|
||||
|
||||
@@ -152,15 +152,17 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
const noOtherAreas = home.areas.length === 0;
|
||||
const noFloor = home.floors.length === 0;
|
||||
|
||||
// Other areas / Areas / Others / nothing
|
||||
const heading =
|
||||
noFloor && noOtherAreas
|
||||
? undefined
|
||||
: noFloor
|
||||
? hass.localize("ui.panel.lovelace.strategy.home.areas")
|
||||
: noOtherAreas
|
||||
? hass.localize("ui.panel.lovelace.strategy.home.devices")
|
||||
: hass.localize("ui.panel.lovelace.strategy.home.other_areas");
|
||||
// Determine heading based on floor/area configuration
|
||||
let heading: string | undefined;
|
||||
if (noFloor && noOtherAreas) {
|
||||
heading = undefined;
|
||||
} else if (noFloor) {
|
||||
heading = hass.localize("ui.panel.lovelace.strategy.home.areas");
|
||||
} else if (noOtherAreas) {
|
||||
heading = hass.localize("ui.panel.lovelace.strategy.home.devices");
|
||||
} else {
|
||||
heading = hass.localize("ui.panel.lovelace.strategy.home.other_areas");
|
||||
}
|
||||
|
||||
floorsSections.push({
|
||||
type: "grid",
|
||||
@@ -242,7 +244,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
|
||||
const hasEnergy =
|
||||
energyPrefs?.energy_sources.some(
|
||||
(source) => source.type === "grid" && source.flow_from.length > 0
|
||||
(source) => source.type === "grid" && !!source.stat_energy_from
|
||||
) ?? false;
|
||||
|
||||
// Build summary cards (used in both mobile section and sidebar)
|
||||
|
||||
@@ -3793,41 +3793,53 @@
|
||||
"title": "Electricity grid",
|
||||
"sub": "Configure the amount of energy that you consume from the grid and, if you produce energy, give back to the grid. This allows Home Assistant to track your whole home energy usage.",
|
||||
"learn_more": "More information on how to get started.",
|
||||
"grid_consumption": "Grid consumption",
|
||||
"edit_consumption": "Edit consumption",
|
||||
"delete_consumption": "Remove consumption",
|
||||
"add_consumption": "Add consumption",
|
||||
"return_to_grid": "Return to grid",
|
||||
"edit_return": "Edit return",
|
||||
"delete_return": "Remove return",
|
||||
"add_return": "Add return",
|
||||
"grid_connections": "Grid connections",
|
||||
"add_connection": "Add grid connection",
|
||||
"edit_connection": "Edit grid connection",
|
||||
"delete_connection": "Delete grid connection",
|
||||
"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",
|
||||
"sensor_type": "Type of power measurement",
|
||||
"type_standard": "Standard",
|
||||
"type_inverted": "Inverted",
|
||||
"dialog": {
|
||||
"header": "Configure grid connection",
|
||||
"energy": "Energy",
|
||||
"energy_from_grid": "Energy imported from grid",
|
||||
"energy_from_helper": "Pick a sensor which measures grid import in either of {unit}.",
|
||||
"energy_to_grid": "Energy exported to grid",
|
||||
"energy_to_helper": "Pick a sensor which measures grid export in either of {unit}.",
|
||||
"import_cost": "Cost tracking",
|
||||
"import_cost_para": "Select how Home Assistant should keep track of the costs of the imported energy.",
|
||||
"no_cost_tracking": "Do not track costs",
|
||||
"cost_stat": "Use an entity tracking the total costs",
|
||||
"cost_stat_label": "Entity with the total costs",
|
||||
"cost_entity": "Use an entity with current price",
|
||||
"cost_entity_label": "Entity with the current price",
|
||||
"cost_number": "Use a static price",
|
||||
"cost_number_label": "Price",
|
||||
"export_compensation": "Export compensation",
|
||||
"export_compensation_para": "Do you get money back when you export energy to the grid?",
|
||||
"no_compensation_tracking": "Do not track compensation",
|
||||
"compensation_stat": "Use an entity tracking the total compensation",
|
||||
"compensation_stat_label": "Entity with the total compensation",
|
||||
"compensation_entity": "Use an entity with current rate",
|
||||
"compensation_entity_label": "Entity with the current rate",
|
||||
"compensation_number": "Use a static rate",
|
||||
"compensation_number_label": "Rate",
|
||||
"power": "Power measurement",
|
||||
"sensor_type": "[%key:ui::panel::config::energy::battery::dialog::sensor_type%]",
|
||||
"sensor_type_para": "[%key:ui::panel::config::energy::battery::dialog::sensor_type_para%]",
|
||||
"type_none": "[%key:ui::panel::config::energy::battery::dialog::type_none%]",
|
||||
"type_standard": "[%key:ui::panel::config::energy::battery::dialog::type_standard%]",
|
||||
"type_inverted": "[%key:ui::panel::config::energy::battery::dialog::type_inverted%]",
|
||||
"type_inverted_description": "Positive values indicate exporting to the grid, negative values indicate importing from the grid.",
|
||||
"type_two_sensors": "Two sensors",
|
||||
"type_two_sensors": "[%key:ui::panel::config::energy::battery::dialog::type_two_sensors%]",
|
||||
"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.",
|
||||
"power_from_grid": "Power from grid",
|
||||
"power_to_grid": "Power to grid"
|
||||
"power_helper": "Pick a sensor which measures grid power in either of {unit}.",
|
||||
"power_from": "Power imported from grid",
|
||||
"power_to": "Power exported to grid"
|
||||
},
|
||||
"flow_dialog": {
|
||||
"cost_entity_helper": "Any sensor with a unit of `{currency}/(valid energy unit)` (e.g. `{currency}/Wh` or `{currency}/kWh`) may be used and will be automatically converted.",
|
||||
"from": {
|
||||
"header": "Configure grid consumption",
|
||||
"paragraph": "Grid consumption is the energy that flows from the energy grid to your home.",
|
||||
"entity_para": "Pick a sensor which measures grid consumption in either of {unit}.",
|
||||
"energy_stat": "Consumed energy",
|
||||
"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": "Entity with the total costs",
|
||||
@@ -3835,20 +3847,6 @@
|
||||
"cost_entity_input": "Entity with the current price",
|
||||
"cost_number": "Use a static price",
|
||||
"cost_number_input": "Price"
|
||||
},
|
||||
"to": {
|
||||
"header": "Configure grid production",
|
||||
"paragraph": "Grid production is the energy that flows from your solar panels to the grid.",
|
||||
"entity_para": "Pick a sensor which measures grid production in either of {unit}.",
|
||||
"energy_stat": "Energy returned to the grid",
|
||||
"cost_para": "Do you get money back when you return energy to the grid?",
|
||||
"no_cost": "I do not get money back",
|
||||
"cost_stat": "Use an entity tracking the total received money",
|
||||
"cost_stat_input": "Entity with the total compensation",
|
||||
"cost_entity": "Use an entity with current rate",
|
||||
"cost_entity_input": "Entity with the current rate",
|
||||
"cost_number": "Use a static rate",
|
||||
"cost_number_input": "Rate"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3892,13 +3890,14 @@
|
||||
"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.",
|
||||
"sensor_type": "Type of power measurement",
|
||||
"sensor_type_para": "Power sensors show the rate of energy flow (in watts), while energy sensors show total consumption (in kWh). Adding a power sensor enables real-time monitoring.",
|
||||
"type_none": "No power sensor",
|
||||
"type_standard": "Standard",
|
||||
"type_inverted": "Inverted",
|
||||
"type_inverted_description": "Positive values indicate charging, negative values indicate discharging.",
|
||||
"type_two_sensors": "Two sensors",
|
||||
"power_discharge": "Discharge power",
|
||||
"power_charge": "Charge power"
|
||||
"power_from": "Discharge power",
|
||||
"power_to": "Charge power"
|
||||
}
|
||||
},
|
||||
"gas": {
|
||||
|
||||
Reference in New Issue
Block a user