mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-13 01:50:44 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01aff39431 |
@@ -22,6 +22,7 @@ import {
|
||||
import type { DateRange } from "../common/datetime/calc_date_range";
|
||||
import { calcDateRange } from "../common/datetime/calc_date_range";
|
||||
import { formatTime24h } from "../common/datetime/format_time";
|
||||
import { DEFAULT_ENTITY_NAME } from "../common/entity/compute_entity_name_display";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { normalizeValueBySIPrefix } from "../common/number/normalize-by-si-prefix";
|
||||
import { groupBy } from "../common/util/group-by";
|
||||
@@ -36,6 +37,7 @@ import type {
|
||||
import {
|
||||
fetchStatistics,
|
||||
getDisplayUnit,
|
||||
getStatisticLabel,
|
||||
getStatisticMetadata,
|
||||
VOLUME_UNITS,
|
||||
} from "./recorder";
|
||||
@@ -311,6 +313,59 @@ export interface EnergySourceByType {
|
||||
export const energySourcesByType = (prefs: EnergyPreferences) =>
|
||||
groupBy(prefs.energy_sources, (item) => item.type) as EnergySourceByType;
|
||||
|
||||
/**
|
||||
* Display name of a configured statistic. A name set by the user always wins;
|
||||
* otherwise the entity is named the same way the rest of the UI names
|
||||
* entities, so devices sharing an entity name stay distinguishable.
|
||||
* Statistics without an entity (external or removed) keep the statistic label.
|
||||
*/
|
||||
export const computeEnergyLabel = (
|
||||
hass: HomeAssistant,
|
||||
statisticId: string,
|
||||
statisticsMetaData?: StatisticsMetaData,
|
||||
customName?: string
|
||||
): string => {
|
||||
if (customName) {
|
||||
return customName;
|
||||
}
|
||||
|
||||
const stateObj = hass.states[statisticId];
|
||||
|
||||
if (stateObj) {
|
||||
return hass.formatEntityName(stateObj, DEFAULT_ENTITY_NAME);
|
||||
}
|
||||
|
||||
return getStatisticLabel(hass, statisticId, statisticsMetaData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Device labels keyed by statistic id. Cards that show live power or flow
|
||||
* key their nodes by `stat_rate` instead of `stat_consumption`; devices
|
||||
* without the requested statistic are left out.
|
||||
*/
|
||||
export const computeEnergyDeviceLabels = (
|
||||
hass: HomeAssistant,
|
||||
devices: DeviceConsumptionEnergyPreference[],
|
||||
statsMetadata?: Record<string, StatisticsMetaData>,
|
||||
statisticKey: "stat_consumption" | "stat_rate" = "stat_consumption"
|
||||
): Record<string, string> => {
|
||||
const labels: Record<string, string> = {};
|
||||
|
||||
for (const device of devices) {
|
||||
const statisticId = device[statisticKey];
|
||||
if (statisticId) {
|
||||
labels[statisticId] = computeEnergyLabel(
|
||||
hass,
|
||||
statisticId,
|
||||
statsMetadata?.[statisticId],
|
||||
device.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
};
|
||||
|
||||
export interface EnergyData {
|
||||
start: Date;
|
||||
end?: Date;
|
||||
|
||||
@@ -11,6 +11,8 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import { getEntityAreaId } from "../../../../common/entity/context/get_entity_context";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-icon-button";
|
||||
@@ -23,9 +25,11 @@ import type {
|
||||
EnergyPreferencesValidation,
|
||||
EnergyValidationIssue,
|
||||
} from "../../../../data/energy";
|
||||
import { saveEnergyPreferences } from "../../../../data/energy";
|
||||
import {
|
||||
computeEnergyLabel,
|
||||
saveEnergyPreferences,
|
||||
} from "../../../../data/energy";
|
||||
import type { StatisticsMetaData } from "../../../../data/recorder";
|
||||
import { getStatisticLabel } from "../../../../data/recorder";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -104,18 +108,7 @@ export class EnergyDeviceSettingsWater extends LitElement {
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
<span class="content"
|
||||
>${
|
||||
device.name ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
device.stat_consumption,
|
||||
this.statsMetadata?.[
|
||||
device.stat_consumption
|
||||
]
|
||||
)
|
||||
}</span
|
||||
>
|
||||
${this._renderName(device)}
|
||||
${this._renderIssueIndicator(
|
||||
this.validationResult?.device_consumption_water[
|
||||
index
|
||||
@@ -155,6 +148,32 @@ export class EnergyDeviceSettingsWater extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderName(device: DeviceConsumptionEnergyPreference) {
|
||||
const name = computeEnergyLabel(
|
||||
this.hass,
|
||||
device.stat_consumption,
|
||||
this.statsMetadata?.[device.stat_consumption],
|
||||
device.name
|
||||
);
|
||||
const areaId = getEntityAreaId(
|
||||
device.stat_consumption,
|
||||
this.hass.entities,
|
||||
this.hass.devices
|
||||
);
|
||||
const area = areaId ? this.hass.areas[areaId] : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
return html`
|
||||
<div class="content">
|
||||
<span class="label">${name}</span>
|
||||
${
|
||||
areaName
|
||||
? html`<span class="label secondary">${areaName}</span>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderIssueIndicator(
|
||||
issues: EnergyValidationIssue[] | undefined,
|
||||
index: number
|
||||
@@ -280,6 +299,22 @@ export class EnergyDeviceSettingsWater extends LitElement {
|
||||
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;
|
||||
}
|
||||
.handle {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
|
||||
@@ -11,6 +11,8 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import { getEntityAreaId } from "../../../../common/entity/context/get_entity_context";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-icon-button";
|
||||
@@ -23,9 +25,11 @@ import type {
|
||||
EnergyPreferencesValidation,
|
||||
EnergyValidationIssue,
|
||||
} from "../../../../data/energy";
|
||||
import { saveEnergyPreferences } from "../../../../data/energy";
|
||||
import {
|
||||
computeEnergyLabel,
|
||||
saveEnergyPreferences,
|
||||
} from "../../../../data/energy";
|
||||
import type { StatisticsMetaData } from "../../../../data/recorder";
|
||||
import { getStatisticLabel } from "../../../../data/recorder";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -104,18 +108,7 @@ export class EnergyDeviceSettings extends LitElement {
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
<span class="content"
|
||||
>${
|
||||
device.name ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
device.stat_consumption,
|
||||
this.statsMetadata?.[
|
||||
device.stat_consumption
|
||||
]
|
||||
)
|
||||
}</span
|
||||
>
|
||||
${this._renderName(device)}
|
||||
${this._renderIssueIndicator(
|
||||
this.validationResult?.device_consumption[
|
||||
index
|
||||
@@ -155,6 +148,32 @@ export class EnergyDeviceSettings extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderName(device: DeviceConsumptionEnergyPreference) {
|
||||
const name = computeEnergyLabel(
|
||||
this.hass,
|
||||
device.stat_consumption,
|
||||
this.statsMetadata?.[device.stat_consumption],
|
||||
device.name
|
||||
);
|
||||
const areaId = getEntityAreaId(
|
||||
device.stat_consumption,
|
||||
this.hass.entities,
|
||||
this.hass.devices
|
||||
);
|
||||
const area = areaId ? this.hass.areas[areaId] : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
return html`
|
||||
<div class="content">
|
||||
<span class="label">${name}</span>
|
||||
${
|
||||
areaName
|
||||
? html`<span class="label secondary">${areaName}</span>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderIssueIndicator(
|
||||
issues: EnergyValidationIssue[] | undefined,
|
||||
index: number
|
||||
@@ -276,6 +295,22 @@ export class EnergyDeviceSettings extends LitElement {
|
||||
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;
|
||||
}
|
||||
.handle {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
|
||||
@@ -11,9 +11,11 @@ import "../../../../components/input/ha-input";
|
||||
import "./ha-energy-upstream-device-picker";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
|
||||
import { energyStatisticHelpUrl } from "../../../../data/energy";
|
||||
import {
|
||||
getStatisticLabel,
|
||||
computeEnergyLabel,
|
||||
energyStatisticHelpUrl,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
getStatisticMetadata,
|
||||
isExternalStatistic,
|
||||
} from "../../../../data/recorder";
|
||||
@@ -174,7 +176,7 @@ export class DialogEnergyDeviceSettingsWater
|
||||
.value=${this._device?.name || ""}
|
||||
.placeholder=${
|
||||
this._device
|
||||
? getStatisticLabel(
|
||||
? computeEnergyLabel(
|
||||
this.hass,
|
||||
this._device.stat_consumption,
|
||||
this._params?.statsMetadata?.[this._device.stat_consumption]
|
||||
|
||||
@@ -11,9 +11,11 @@ import "../../../../components/input/ha-input";
|
||||
import "./ha-energy-upstream-device-picker";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
|
||||
import { energyStatisticHelpUrl } from "../../../../data/energy";
|
||||
import {
|
||||
getStatisticLabel,
|
||||
computeEnergyLabel,
|
||||
energyStatisticHelpUrl,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
getStatisticMetadata,
|
||||
isExternalStatistic,
|
||||
} from "../../../../data/recorder";
|
||||
@@ -170,7 +172,7 @@ export class DialogEnergyDeviceSettings
|
||||
.value=${this._device?.name || ""}
|
||||
.placeholder=${
|
||||
this._device
|
||||
? getStatisticLabel(
|
||||
? computeEnergyLabel(
|
||||
this.hass,
|
||||
this._device.stat_consumption,
|
||||
this._params?.statsMetadata?.[this._device.stat_consumption]
|
||||
|
||||
@@ -7,7 +7,6 @@ import memoizeOne from "memoize-one";
|
||||
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../../../common/util/compute_rtl";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import "../../../../components/ha-combo-box-item";
|
||||
import "../../../../components/ha-generic-picker";
|
||||
@@ -15,6 +14,7 @@ import type { PickerComboBoxItem } from "../../../../components/ha-picker-combo-
|
||||
import type { PickerValueRenderer } from "../../../../components/ha-picker-field";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
|
||||
import { computeEnergyLabel } from "../../../../data/energy";
|
||||
import { domainToName } from "../../../../data/integration";
|
||||
import {
|
||||
getStatisticLabel,
|
||||
@@ -73,20 +73,18 @@ export class HaEnergyUpstreamDevicePicker extends LitElement {
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
return {
|
||||
id: statisticId,
|
||||
primary: name || entityName || deviceName || statisticId,
|
||||
secondary,
|
||||
// Match the label shown in the device list and the graphs.
|
||||
primary: computeEnergyLabel(
|
||||
this.hass,
|
||||
statisticId,
|
||||
this.statsMetadata?.[statisticId],
|
||||
name
|
||||
),
|
||||
secondary: areaName,
|
||||
stateObj,
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
|
||||
@@ -8,13 +8,13 @@ import type {
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
computeConsumptionData,
|
||||
computeEnergyDeviceLabels,
|
||||
getSuggestedPeriod,
|
||||
getSummedData,
|
||||
} from "../../../../data/energy";
|
||||
import type { Statistics, StatisticsMetaData } from "../../../../data/recorder";
|
||||
import type { Statistics } from "../../../../data/recorder";
|
||||
import {
|
||||
calculateStatisticSumGrowth,
|
||||
getStatisticLabel,
|
||||
isExternalStatistic,
|
||||
} from "../../../../data/recorder";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -69,13 +69,13 @@ interface ProcessContext {
|
||||
end: Date;
|
||||
compareStart?: Date;
|
||||
untrackedOrder: number;
|
||||
deviceLabels: Record<string, string>;
|
||||
}
|
||||
|
||||
function processDataSet(
|
||||
ctx: ProcessContext,
|
||||
computedStyle: CSSStyleDeclaration,
|
||||
statistics: Statistics,
|
||||
statisticsMetaData: Record<string, StatisticsMetaData>,
|
||||
devices: DeviceConsumptionEnergyPreference[],
|
||||
sorted_devices: string[],
|
||||
childMap: Record<string, string[]>,
|
||||
@@ -167,12 +167,7 @@ function processDataSet(
|
||||
}
|
||||
|
||||
const name =
|
||||
(source.name ||
|
||||
getStatisticLabel(
|
||||
ctx.hass,
|
||||
source.stat_consumption,
|
||||
statisticsMetaData[source.stat_consumption]
|
||||
)) +
|
||||
ctx.deviceLabels[source.stat_consumption] +
|
||||
(source.stat_consumption in childMap
|
||||
? ` (${ctx.hass.localize("ui.panel.lovelace.cards.energy.energy_devices_detail_graph.untracked")})`
|
||||
: "");
|
||||
@@ -351,6 +346,8 @@ export function generateEnergyDevicesDetailGraphData(
|
||||
const data = energyData.stats;
|
||||
const compareData = energyData.statsCompare;
|
||||
|
||||
const devices = energyData.prefs.device_consumption;
|
||||
|
||||
const ctx: ProcessContext = {
|
||||
hass,
|
||||
config,
|
||||
@@ -358,10 +355,13 @@ export function generateEnergyDevicesDetailGraphData(
|
||||
end,
|
||||
compareStart,
|
||||
untrackedOrder,
|
||||
deviceLabels: computeEnergyDeviceLabels(
|
||||
hass,
|
||||
devices,
|
||||
energyData.statsMetadata
|
||||
),
|
||||
};
|
||||
|
||||
const devices = energyData.prefs.device_consumption;
|
||||
|
||||
const childMap: Record<string, string[]> = {};
|
||||
devices.forEach((d) => {
|
||||
if (d.included_in_stat) {
|
||||
@@ -425,7 +425,6 @@ export function generateEnergyDevicesDetailGraphData(
|
||||
ctx,
|
||||
computedStyles,
|
||||
compareData,
|
||||
energyData.statsMetadata,
|
||||
energyData.prefs.device_consumption,
|
||||
sorted_devices,
|
||||
childMap,
|
||||
@@ -468,7 +467,6 @@ export function generateEnergyDevicesDetailGraphData(
|
||||
ctx,
|
||||
computedStyles,
|
||||
data,
|
||||
energyData.statsMetadata,
|
||||
energyData.prefs.device_consumption,
|
||||
sorted_devices,
|
||||
childMap,
|
||||
|
||||
@@ -16,6 +16,7 @@ import "../../../../components/chart/ha-chart-tooltip-marker";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import {
|
||||
computeConsumptionData,
|
||||
computeEnergyDeviceLabels,
|
||||
getEnergyDataCollection,
|
||||
getSummedData,
|
||||
validateEnergyCollectionKey,
|
||||
@@ -91,6 +92,8 @@ export class HuiEnergyDevicesGraphCard
|
||||
|
||||
private _compoundStats: string[] = [];
|
||||
|
||||
private _deviceLabels: Record<string, string> = {};
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
@@ -295,9 +298,8 @@ export class HuiEnergyDevicesGraphCard
|
||||
? ` (${this.hass.localize("ui.panel.lovelace.cards.energy.energy_devices_graph.untracked")})`
|
||||
: "";
|
||||
return (
|
||||
(this._data?.prefs.device_consumption.find(
|
||||
(d) => d.stat_consumption === statisticId
|
||||
)?.name ||
|
||||
// The untracked slice is not a statistic, so it has no label.
|
||||
(this._deviceLabels[statisticId] ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
statisticId,
|
||||
@@ -377,6 +379,12 @@ export class HuiEnergyDevicesGraphCard
|
||||
.map((d) => d.included_in_stat)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
this._deviceLabels = computeEnergyDeviceLabels(
|
||||
this.hass,
|
||||
energyData.prefs.device_consumption,
|
||||
energyData.statsMetadata
|
||||
);
|
||||
|
||||
const devices = energyData.prefs.device_consumption;
|
||||
const devicesTotals: Record<string, number> = {};
|
||||
devices.forEach((device) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import "../../../../components/ha-svg-icon";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import {
|
||||
computeConsumptionData,
|
||||
computeEnergyDeviceLabels,
|
||||
energySourcesByType,
|
||||
getEnergyDataCollection,
|
||||
getSummedData,
|
||||
@@ -272,8 +273,14 @@ class HuiEnergySankeyCard
|
||||
? calculateStatisticSumGrowth(this._data!.stats[statConsumption]) || 0
|
||||
: 0;
|
||||
|
||||
const deviceLabel = (statConsumption: string, name?: string) =>
|
||||
name ||
|
||||
const deviceLabels = computeEnergyDeviceLabels(
|
||||
this.hass,
|
||||
prefs.device_consumption,
|
||||
this._data.statsMetadata
|
||||
);
|
||||
|
||||
const deviceLabel = (statConsumption: string) =>
|
||||
deviceLabels[statConsumption] ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
statConsumption,
|
||||
|
||||
@@ -7,6 +7,7 @@ import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { EnergyData, EnergyPreferences } from "../../../../data/energy";
|
||||
import {
|
||||
computeEnergyDeviceLabels,
|
||||
formatPowerShort,
|
||||
getEnergyDataCollection,
|
||||
getPowerFromState,
|
||||
@@ -278,6 +279,13 @@ class HuiPowerSankeyCard
|
||||
}
|
||||
}
|
||||
|
||||
const deviceLabels = computeEnergyDeviceLabels(
|
||||
this.hass,
|
||||
prefs.device_consumption,
|
||||
this._data.statsMetadata,
|
||||
"stat_rate"
|
||||
);
|
||||
|
||||
const {
|
||||
deviceNodes,
|
||||
parentLinks,
|
||||
@@ -294,7 +302,7 @@ class HuiPowerSankeyCard
|
||||
initialUntracked: homeNode.value,
|
||||
getId: (device) => device.stat_rate,
|
||||
getValue: (id) => this._getCurrentPower(id),
|
||||
getLabel: (id, name) => name || this._getEntityLabel(id),
|
||||
getLabel: (id) => deviceLabels[id] || this._getEntityLabel(id),
|
||||
getEntityId: (id) => id,
|
||||
});
|
||||
links.push(...deviceLinks);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { classMap } from "lit/directives/class-map";
|
||||
import "../../../../components/ha-card";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import {
|
||||
computeEnergyDeviceLabels,
|
||||
formatFlowRateShort,
|
||||
getEnergyDataCollection,
|
||||
getFlowRateFromState,
|
||||
@@ -241,6 +242,13 @@ class HuiWaterFlowSankeyCard
|
||||
}
|
||||
}
|
||||
|
||||
const deviceLabels = computeEnergyDeviceLabels(
|
||||
this.hass,
|
||||
prefs.device_consumption_water,
|
||||
this._data.statsMetadata,
|
||||
"stat_rate"
|
||||
);
|
||||
|
||||
const {
|
||||
deviceNodes,
|
||||
parentLinks,
|
||||
@@ -257,7 +265,7 @@ class HuiWaterFlowSankeyCard
|
||||
initialUntracked: effectiveTotalInflow,
|
||||
getId: (device) => device.stat_rate,
|
||||
getValue: (id) => this._getCurrentFlowRate(id),
|
||||
getLabel: (id, name) => name || this._getEntityLabel(id),
|
||||
getLabel: (id) => deviceLabels[id] || this._getEntityLabel(id),
|
||||
getEntityId: (id) => id,
|
||||
});
|
||||
links.push(...deviceLinks);
|
||||
|
||||
@@ -7,6 +7,7 @@ import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import {
|
||||
computeEnergyDeviceLabels,
|
||||
getEnergyDataCollection,
|
||||
validateEnergyCollectionKey,
|
||||
} from "../../../../data/energy";
|
||||
@@ -215,8 +216,14 @@ class HuiWaterSankeyCard
|
||||
? calculateStatisticSumGrowth(this._data!.stats[statConsumption]) || 0
|
||||
: 0;
|
||||
|
||||
const deviceLabel = (statConsumption: string, name?: string) =>
|
||||
name ||
|
||||
const deviceLabels = computeEnergyDeviceLabels(
|
||||
this.hass,
|
||||
prefs.device_consumption_water,
|
||||
this._data!.statsMetadata
|
||||
);
|
||||
|
||||
const deviceLabel = (statConsumption: string) =>
|
||||
deviceLabels[statConsumption] ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
statConsumption,
|
||||
|
||||
@@ -13,13 +13,19 @@ import {
|
||||
} from "../../src/data/translation";
|
||||
import {
|
||||
computeConsumptionSingle,
|
||||
computeEnergyLabel,
|
||||
computeEnergyDeviceLabels,
|
||||
formatConsumptionShort,
|
||||
calculateSolarConsumedGauge,
|
||||
formatPowerShort,
|
||||
getNextEnergyPeriodStart,
|
||||
getEnergyDefaultPeriodStorageKey,
|
||||
} from "../../src/data/energy";
|
||||
import type { DeviceRegistryEntry } from "../../src/data/device/device_registry";
|
||||
import type { EntityRegistryDisplayEntry } from "../../src/data/entity/entity_registry";
|
||||
import type { StatisticsMetaData } from "../../src/data/recorder";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
import { createMockEntityState, createMockHass } from "../fixtures/hass";
|
||||
|
||||
const checkConsumptionResult = (
|
||||
input: {
|
||||
@@ -944,3 +950,151 @@ describe("getEnergyDefaultPeriodStorageKey", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeEnergyLabel", () => {
|
||||
const ENTITY_ID = "sensor.washer_energy";
|
||||
|
||||
const createEntry = (
|
||||
entry: Partial<EntityRegistryDisplayEntry>
|
||||
): EntityRegistryDisplayEntry =>
|
||||
({
|
||||
entity_id: ENTITY_ID,
|
||||
labels: [],
|
||||
...entry,
|
||||
}) as EntityRegistryDisplayEntry;
|
||||
|
||||
const createDevice = (
|
||||
device: Partial<DeviceRegistryEntry>
|
||||
): DeviceRegistryEntry =>
|
||||
({ id: "device1", name_by_user: null, ...device }) as DeviceRegistryEntry;
|
||||
|
||||
const createHass = (
|
||||
friendlyName: string,
|
||||
entry?: Partial<EntityRegistryDisplayEntry>,
|
||||
device?: Partial<DeviceRegistryEntry>
|
||||
) =>
|
||||
createMockHass(
|
||||
{
|
||||
[ENTITY_ID]: createMockEntityState(ENTITY_ID, "1", {
|
||||
friendly_name: friendlyName,
|
||||
}),
|
||||
},
|
||||
{
|
||||
entities: entry ? { [ENTITY_ID]: createEntry(entry) } : {},
|
||||
devices: device ? { device1: createDevice(device) } : {},
|
||||
}
|
||||
);
|
||||
|
||||
it("composes the device and entity name", () => {
|
||||
const hass = createHass(
|
||||
"Washer Energy",
|
||||
{ name: "Energy", device_id: "device1" },
|
||||
{ name: "Washer" }
|
||||
);
|
||||
|
||||
assert.equal(computeEnergyLabel(hass, ENTITY_ID), "Washer Energy");
|
||||
});
|
||||
|
||||
it("uses the device name alone when the entity has no name of its own", () => {
|
||||
const hass = createHass(
|
||||
"Washer",
|
||||
{ name: "Washer", device_id: "device1" },
|
||||
{ name: "Washer" }
|
||||
);
|
||||
|
||||
assert.equal(computeEnergyLabel(hass, ENTITY_ID), "Washer");
|
||||
});
|
||||
|
||||
it("distinguishes entities sharing a name by their device", () => {
|
||||
const hass = createHass(
|
||||
"Energy",
|
||||
{ name: "Energy", device_id: "device1" },
|
||||
{ name: "Dishwasher" }
|
||||
);
|
||||
|
||||
assert.equal(computeEnergyLabel(hass, ENTITY_ID), "Dishwasher Energy");
|
||||
});
|
||||
|
||||
it("keeps a name set by the user", () => {
|
||||
const hass = createHass(
|
||||
"Washer Energy",
|
||||
{ name: "Energy", device_id: "device1" },
|
||||
{ name: "Washer" }
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
computeEnergyLabel(hass, ENTITY_ID, undefined, "Laundry"),
|
||||
"Laundry"
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores an empty name", () => {
|
||||
const hass = createHass(
|
||||
"Washer Energy",
|
||||
{ name: "Energy", device_id: "device1" },
|
||||
{ name: "Washer" }
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
computeEnergyLabel(hass, ENTITY_ID, undefined, ""),
|
||||
"Washer Energy"
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the friendly name for an entity outside the registry", () => {
|
||||
const hass = createHass("Washer Energy");
|
||||
|
||||
assert.equal(computeEnergyLabel(hass, ENTITY_ID), "Washer Energy");
|
||||
});
|
||||
|
||||
it("uses the statistic metadata name when there is no entity", () => {
|
||||
const hass = createMockHass();
|
||||
|
||||
assert.equal(
|
||||
computeEnergyLabel(hass, "external:solar", {
|
||||
statistic_id: "external:solar",
|
||||
name: "Solar production",
|
||||
} as StatisticsMetaData),
|
||||
"Solar production"
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the statistic id when there is nothing to name it with", () => {
|
||||
const hass = createMockHass();
|
||||
|
||||
assert.equal(computeEnergyLabel(hass, "external:solar"), "external:solar");
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeEnergyDeviceLabels", () => {
|
||||
const DEVICES = [
|
||||
{
|
||||
stat_consumption: "sensor.washer_energy",
|
||||
stat_rate: "sensor.washer_power",
|
||||
},
|
||||
{ stat_consumption: "sensor.heater_energy", name: "Heater" },
|
||||
];
|
||||
|
||||
const hass = createMockHass({
|
||||
"sensor.washer_energy": createMockEntityState("sensor.washer_energy", "1", {
|
||||
friendly_name: "Washer Energy",
|
||||
}),
|
||||
"sensor.washer_power": createMockEntityState("sensor.washer_power", "5", {
|
||||
friendly_name: "Washer Power",
|
||||
}),
|
||||
});
|
||||
|
||||
it("keys labels by the consumption statistic", () => {
|
||||
assert.deepEqual(computeEnergyDeviceLabels(hass, DEVICES), {
|
||||
"sensor.washer_energy": "Washer Energy",
|
||||
"sensor.heater_energy": "Heater",
|
||||
});
|
||||
});
|
||||
|
||||
it("keys labels by the rate statistic, skipping devices without one", () => {
|
||||
assert.deepEqual(
|
||||
computeEnergyDeviceLabels(hass, DEVICES, undefined, "stat_rate"),
|
||||
{ "sensor.washer_power": "Washer Power" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+32
-9
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* Deterministic `HomeAssistant` stub covering exactly what the chart data
|
||||
* transforms read: states, entities, locale, config, localize, and entity
|
||||
* state formatting. Everything is stable across runs.
|
||||
* transforms read: states, registries, locale, config, localize, and entity
|
||||
* state/name formatting. Everything is stable across runs.
|
||||
*/
|
||||
import type { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeEntityNameDisplay } from "../../src/common/entity/compute_entity_name_display";
|
||||
import type { LocalizeFunc } from "../../src/common/translations/localize";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -43,21 +44,43 @@ export const createMockEntityState = (
|
||||
context: { id: "fixture", parent_id: null, user_id: null },
|
||||
});
|
||||
|
||||
export const createMockHass = (states: HassEntities = {}): HomeAssistant =>
|
||||
({
|
||||
export const createMockHass = (
|
||||
states: HassEntities = {},
|
||||
registries: Partial<
|
||||
Pick<HomeAssistant, "entities" | "devices" | "areas" | "floors">
|
||||
> = {}
|
||||
): HomeAssistant => {
|
||||
const entities = registries.entities ?? {};
|
||||
const devices = registries.devices ?? {};
|
||||
const areas = registries.areas ?? {};
|
||||
const floors = registries.floors ?? {};
|
||||
|
||||
return {
|
||||
states,
|
||||
entities: {},
|
||||
devices: {},
|
||||
areas: {},
|
||||
floors: {},
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors,
|
||||
config: demoConfig,
|
||||
locale: mockLocale,
|
||||
language: "en",
|
||||
localize: mockLocalize,
|
||||
translationMetadata: { translations: {} },
|
||||
formatEntityState: (stateObj: HassEntity, state?: string) =>
|
||||
state ?? stateObj.state,
|
||||
formatEntityAttributeValue: (stateObj: HassEntity, attribute: string) =>
|
||||
String(stateObj.attributes[attribute]),
|
||||
formatEntityAttributeName: (_stateObj: HassEntity, attribute: string) =>
|
||||
attribute,
|
||||
}) as unknown as HomeAssistant;
|
||||
formatEntityName: ((stateObj, name, options) =>
|
||||
computeEntityNameDisplay(
|
||||
stateObj,
|
||||
name,
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors,
|
||||
options
|
||||
)) satisfies HomeAssistant["formatEntityName"],
|
||||
} as unknown as HomeAssistant;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user