mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-18 07:20:24 +00:00
Compare commits
5 Commits
water-sank
...
add-device
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9331282521 | ||
|
|
9299b84708 | ||
|
|
df7a36e743 | ||
|
|
5786fe4b8d | ||
|
|
6fa274e4bf |
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
|
||||
@@ -84,7 +84,6 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||
stat_consumption: "sensor.energy_boiler",
|
||||
},
|
||||
],
|
||||
device_consumption_water: [],
|
||||
})
|
||||
);
|
||||
hass.mockWS(
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
"home-assistant-js-websocket": "9.5.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.18",
|
||||
"js-yaml": "4.1.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
|
||||
@@ -30,7 +30,6 @@ export class HaFilterChip extends FilterChip {
|
||||
var(--rgb-primary-text-color),
|
||||
0.15
|
||||
);
|
||||
--_label-text-font: var(--ha-font-family-body);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -34,7 +34,6 @@ class SearchInput extends LitElement {
|
||||
return html`
|
||||
<ha-textfield
|
||||
.autofocus=${this.autofocus}
|
||||
autocomplete="off"
|
||||
.label=${this.label || this.hass.localize("ui.common.search")}
|
||||
.value=${this.filter || ""}
|
||||
icon
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface AnalyticsPreferences {
|
||||
diagnostics?: boolean;
|
||||
usage?: boolean;
|
||||
statistics?: boolean;
|
||||
snapshots?: boolean;
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
|
||||
@@ -200,7 +200,6 @@ export type EnergySource =
|
||||
export interface EnergyPreferences {
|
||||
energy_sources: EnergySource[];
|
||||
device_consumption: DeviceConsumptionEnergyPreference[];
|
||||
device_consumption_water: DeviceConsumptionEnergyPreference[];
|
||||
}
|
||||
|
||||
export interface EnergyInfo {
|
||||
@@ -357,11 +356,6 @@ export const getReferencedStatisticIds = (
|
||||
if (!(includeTypes && !includeTypes.includes("device"))) {
|
||||
statIDs.push(...prefs.device_consumption.map((d) => d.stat_consumption));
|
||||
}
|
||||
if (!(includeTypes && !includeTypes.includes("water"))) {
|
||||
statIDs.push(
|
||||
...prefs.device_consumption_water.map((d) => d.stat_consumption)
|
||||
);
|
||||
}
|
||||
|
||||
return statIDs;
|
||||
};
|
||||
|
||||
@@ -1336,7 +1336,7 @@ class DialogAddAutomationElement
|
||||
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||||
--md-list-item-bottom-space: var(--ha-space-1);
|
||||
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-family-body);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
||||
--md-list-item-one-line-container-height: var(--ha-space-10);
|
||||
}
|
||||
ha-bottom-sheet .groups {
|
||||
@@ -1400,7 +1400,7 @@ class DialogAddAutomationElement
|
||||
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||||
--md-list-item-bottom-space: var(--ha-space-2);
|
||||
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-family-body);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-0) var(--ha-space-4);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { mdiOpenInNew } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-analytics";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { Analytics } from "../../../data/analytics";
|
||||
import {
|
||||
getAnalyticsDetails,
|
||||
@@ -17,6 +13,8 @@ import {
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { isDevVersion } from "../../../common/config/version";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
|
||||
@customElement("ha-config-analytics")
|
||||
class ConfigAnalytics extends LitElement {
|
||||
@@ -34,10 +32,22 @@ class ConfigAnalytics extends LitElement {
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize("ui.panel.config.analytics.header") ||
|
||||
"Home Assistant analytics"}
|
||||
>
|
||||
<div class="card-content">
|
||||
${error ? html`<div class="error">${error}</div>` : ""}
|
||||
<p>${this.hass.localize("ui.panel.config.analytics.intro")}</p>
|
||||
${error ? html`<div class="error">${error}</div>` : nothing}
|
||||
<p>
|
||||
${this.hass.localize("ui.panel.config.analytics.intro")}
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/integrations/analytics/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize("ui.panel.config.analytics.learn_more")}</a
|
||||
>.
|
||||
</p>
|
||||
<ha-analytics
|
||||
translation_key_panel="config"
|
||||
@analytics-preferences-changed=${this._preferencesChanged}
|
||||
@@ -45,26 +55,50 @@ class ConfigAnalytics extends LitElement {
|
||||
.analytics=${this._analyticsDetails}
|
||||
></ha-analytics>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button @click=${this._save}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.save_button"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<div class="footer">
|
||||
<ha-button
|
||||
size="small"
|
||||
appearance="plain"
|
||||
href=${documentationUrl(this.hass, "/integrations/analytics/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.analytics.learn_more")}
|
||||
</ha-button>
|
||||
</div>
|
||||
${isDevVersion(this.hass.config.version)
|
||||
? html`<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.analytics.preferences.snapshots.header"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.analytics.preferences.snapshots.info"
|
||||
)}
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/device-database/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.analytics.preferences.snapshots.learn_more"
|
||||
)}</a
|
||||
>.
|
||||
</p>
|
||||
<ha-settings-row>
|
||||
<span slot="heading" data-for="snapshots">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.analytics.preferences.snapshots.title`
|
||||
)}
|
||||
</span>
|
||||
<span slot="description" data-for="snapshots">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.analytics.preferences.snapshots.description`
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._handleDeviceRowClick}
|
||||
.checked=${!!this._analyticsDetails?.preferences.snapshots}
|
||||
.disabled=${this._analyticsDetails === undefined}
|
||||
name="snapshots"
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
</div>
|
||||
</ha-card>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -96,11 +130,25 @@ class ConfigAnalytics extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleDeviceRowClick(ev: Event) {
|
||||
const target = ev.target as HaSwitch;
|
||||
|
||||
this._analyticsDetails = {
|
||||
...this._analyticsDetails!,
|
||||
preferences: {
|
||||
...this._analyticsDetails!.preferences,
|
||||
snapshots: target.checked,
|
||||
},
|
||||
};
|
||||
this._save();
|
||||
}
|
||||
|
||||
private _preferencesChanged(event: CustomEvent): void {
|
||||
this._analyticsDetails = {
|
||||
...this._analyticsDetails!,
|
||||
preferences: event.detail.preferences,
|
||||
};
|
||||
this._save();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -117,21 +165,10 @@ class ConfigAnalytics extends LitElement {
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
ha-card:not(:first-of-type) {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.footer {
|
||||
padding: 32px 0 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ha-button[size="small"] ha-svg-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
`, // row-reverse so we tab first to "save"
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiWater,
|
||||
mdiDragHorizontalVariant,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type {
|
||||
DeviceConsumptionEnergyPreference,
|
||||
EnergyPreferences,
|
||||
EnergyPreferencesValidation,
|
||||
} from "../../../../data/energy";
|
||||
import { saveEnergyPreferences } from "../../../../data/energy";
|
||||
import type { StatisticsMetaData } from "../../../../data/recorder";
|
||||
import { getStatisticLabel } from "../../../../data/recorder";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { documentationUrl } from "../../../../util/documentation-url";
|
||||
import { showEnergySettingsDeviceWaterDialog } from "../dialogs/show-dialogs-energy";
|
||||
import "./ha-energy-validation-result";
|
||||
import { energyCardStyles } from "./styles";
|
||||
|
||||
@customElement("ha-energy-device-settings-water")
|
||||
export class EnergyDeviceSettingsWater extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public preferences!: EnergyPreferences;
|
||||
|
||||
@property({ attribute: false })
|
||||
public statsMetadata?: Record<string, StatisticsMetaData>;
|
||||
|
||||
@property({ attribute: false })
|
||||
public validationResult?: EnergyPreferencesValidation;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<h1 class="card-header">
|
||||
<ha-svg-icon .path=${mdiWater}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.title"
|
||||
)}
|
||||
</h1>
|
||||
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.sub"
|
||||
)}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/docs/energy/water/#individual-devices"
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.learn_more"
|
||||
)}</a
|
||||
>
|
||||
</p>
|
||||
${this.validationResult?.device_consumption.map(
|
||||
(result) => html`
|
||||
<ha-energy-validation-result
|
||||
.hass=${this.hass}
|
||||
.issues=${result}
|
||||
></ha-energy-validation-result>
|
||||
`
|
||||
)}
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.devices"
|
||||
)}
|
||||
</h3>
|
||||
<ha-sortable handle-selector=".handle" @item-moved=${this._itemMoved}>
|
||||
<div class="devices">
|
||||
${repeat(
|
||||
this.preferences.device_consumption_water,
|
||||
(device) => device.stat_consumption,
|
||||
(device) => html`
|
||||
<div class="row" .device=${device}>
|
||||
<div class="handle">
|
||||
<ha-svg-icon
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
<span class="content"
|
||||
>${device.name ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
device.stat_consumption,
|
||||
this.statsMetadata?.[device.stat_consumption]
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
@click=${this._editDevice}
|
||||
.path=${mdiPencil}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.delete")}
|
||||
@click=${this._deleteDevice}
|
||||
.device=${device}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
<div class="row">
|
||||
<ha-svg-icon .path=${mdiWater}></ha-svg-icon>
|
||||
<ha-button
|
||||
@click=${this._addDevice}
|
||||
appearance="filled"
|
||||
size="small"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.add_device"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _itemMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const devices = this.preferences.device_consumption_water.concat();
|
||||
const device = devices.splice(oldIndex, 1)[0];
|
||||
devices.splice(newIndex, 0, device);
|
||||
|
||||
const newPrefs = {
|
||||
...this.preferences,
|
||||
device_consumption_water: devices,
|
||||
};
|
||||
fireEvent(this, "value-changed", { value: newPrefs });
|
||||
this._savePreferences(newPrefs);
|
||||
}
|
||||
|
||||
private _editDevice(ev) {
|
||||
const origDevice: DeviceConsumptionEnergyPreference =
|
||||
ev.currentTarget.closest(".row").device;
|
||||
showEnergySettingsDeviceWaterDialog(this, {
|
||||
statsMetadata: this.statsMetadata,
|
||||
device: { ...origDevice },
|
||||
device_consumptions: this.preferences
|
||||
.device_consumption_water as DeviceConsumptionEnergyPreference[],
|
||||
saveCallback: async (newDevice) => {
|
||||
const newPrefs = {
|
||||
...this.preferences,
|
||||
device_consumption_water:
|
||||
this.preferences.device_consumption_water.map((d) =>
|
||||
d === origDevice ? newDevice : d
|
||||
),
|
||||
};
|
||||
this._sanitizeParents(newPrefs);
|
||||
await this._savePreferences(newPrefs);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _addDevice() {
|
||||
showEnergySettingsDeviceWaterDialog(this, {
|
||||
statsMetadata: this.statsMetadata,
|
||||
device_consumptions: this.preferences
|
||||
.device_consumption_water as DeviceConsumptionEnergyPreference[],
|
||||
saveCallback: async (device) => {
|
||||
await this._savePreferences({
|
||||
...this.preferences,
|
||||
device_consumption_water:
|
||||
this.preferences.device_consumption_water.concat(device),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _sanitizeParents(prefs: EnergyPreferences) {
|
||||
const statIds = prefs.device_consumption_water.map(
|
||||
(d) => d.stat_consumption
|
||||
);
|
||||
prefs.device_consumption_water.forEach((d) => {
|
||||
if (d.included_in_stat && !statIds.includes(d.included_in_stat)) {
|
||||
delete d.included_in_stat;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async _deleteDevice(ev) {
|
||||
const deviceToDelete: DeviceConsumptionEnergyPreference =
|
||||
ev.currentTarget.device;
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.energy.delete_source"),
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newPrefs = {
|
||||
...this.preferences,
|
||||
device_consumption_water:
|
||||
this.preferences.device_consumption_water.filter(
|
||||
(device) => device !== deviceToDelete
|
||||
),
|
||||
};
|
||||
this._sanitizeParents(newPrefs);
|
||||
await this._savePreferences(newPrefs);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, { title: `Failed to save config: ${err.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
private async _savePreferences(preferences: EnergyPreferences) {
|
||||
const result = await saveEnergyPreferences(this.hass, preferences);
|
||||
fireEvent(this, "value-changed", { value: result });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
energyCardStyles,
|
||||
css`
|
||||
.handle {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-energy-device-settings-water": EnergyDeviceSettingsWater;
|
||||
}
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
import { mdiWater } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-select";
|
||||
import "../../../../components/ha-list-item";
|
||||
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
|
||||
import { energyStatisticHelpUrl } from "../../../../data/energy";
|
||||
import { getStatisticLabel } from "../../../../data/recorder";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { EnergySettingsDeviceWaterDialogParams } from "./show-dialogs-energy";
|
||||
|
||||
const volumeUnitClasses = ["volume"];
|
||||
|
||||
@customElement("dialog-energy-device-settings-water")
|
||||
export class DialogEnergyDeviceSettingsWater
|
||||
extends LitElement
|
||||
implements HassDialog<EnergySettingsDeviceWaterDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: EnergySettingsDeviceWaterDialogParams;
|
||||
|
||||
@state() private _device?: DeviceConsumptionEnergyPreference;
|
||||
|
||||
@state() private _volume_units?: string[];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _excludeList?: string[];
|
||||
|
||||
private _possibleParents: DeviceConsumptionEnergyPreference[] = [];
|
||||
|
||||
public async showDialog(
|
||||
params: EnergySettingsDeviceWaterDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._device = this._params.device;
|
||||
this._computePossibleParents();
|
||||
this._volume_units = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "water")
|
||||
).units;
|
||||
this._excludeList = this._params.device_consumptions
|
||||
.map((entry) => entry.stat_consumption)
|
||||
.filter((id) => id !== this._device?.stat_consumption);
|
||||
}
|
||||
|
||||
private _computePossibleParents() {
|
||||
if (!this._device || !this._params) {
|
||||
this._possibleParents = [];
|
||||
return;
|
||||
}
|
||||
const children: string[] = [];
|
||||
const devices = this._params.device_consumptions;
|
||||
function getChildren(stat) {
|
||||
devices.forEach((d) => {
|
||||
if (d.included_in_stat === stat) {
|
||||
children.push(d.stat_consumption);
|
||||
getChildren(d.stat_consumption);
|
||||
}
|
||||
});
|
||||
}
|
||||
getChildren(this._device.stat_consumption);
|
||||
this._possibleParents = this._params.device_consumptions.filter(
|
||||
(d) =>
|
||||
d.stat_consumption !== this._device!.stat_consumption &&
|
||||
d.stat_consumption !== this._params?.device?.stat_consumption &&
|
||||
!children.includes(d.stat_consumption)
|
||||
);
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
this._device = undefined;
|
||||
this._error = undefined;
|
||||
this._excludeList = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const pickableUnit = this._volume_units?.join(", ") || "";
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
.heading=${html`<ha-svg-icon
|
||||
.path=${mdiWater}
|
||||
style="--mdc-icon-size: 32px;"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.dialog.header"
|
||||
)}`}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.dialog.selected_stat_intro",
|
||||
{ unit: pickableUnit }
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||
.includeUnitClass=${volumeUnitClasses}
|
||||
.value=${this._device?.stat_consumption}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.dialog.device_consumption_water"
|
||||
)}
|
||||
.excludeStatistics=${this._excludeList}
|
||||
@value-changed=${this._statisticChanged}
|
||||
dialogInitialFocus
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.dialog.display_name"
|
||||
)}
|
||||
type="text"
|
||||
.disabled=${!this._device}
|
||||
.value=${this._device?.name || ""}
|
||||
.placeholder=${this._device
|
||||
? getStatisticLabel(
|
||||
this.hass,
|
||||
this._device.stat_consumption,
|
||||
this._params?.statsMetadata?.[this._device.stat_consumption]
|
||||
)
|
||||
: ""}
|
||||
@input=${this._nameChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.dialog.included_in_device"
|
||||
)}
|
||||
.value=${this._device?.included_in_stat || ""}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.dialog.included_in_device_helper"
|
||||
)}
|
||||
.disabled=${!this._device}
|
||||
@selected=${this._parentSelected}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
clearable
|
||||
>
|
||||
${!this._possibleParents.length
|
||||
? html`
|
||||
<ha-list-item disabled value="-"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.dialog.no_upstream_devices"
|
||||
)}</ha-list-item
|
||||
>
|
||||
`
|
||||
: this._possibleParents.map(
|
||||
(stat) => html`
|
||||
<ha-list-item .value=${stat.stat_consumption}
|
||||
>${stat.name ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
stat.stat_consumption,
|
||||
this._params?.statsMetadata?.[stat.stat_consumption]
|
||||
)}</ha-list-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._device}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _statisticChanged(ev: CustomEvent<{ value: string }>) {
|
||||
if (!ev.detail.value) {
|
||||
this._device = undefined;
|
||||
return;
|
||||
}
|
||||
this._device = { stat_consumption: ev.detail.value };
|
||||
this._computePossibleParents();
|
||||
}
|
||||
|
||||
private _nameChanged(ev) {
|
||||
const newDevice = {
|
||||
...this._device!,
|
||||
name: ev.target!.value,
|
||||
} as DeviceConsumptionEnergyPreference;
|
||||
if (!newDevice.name) {
|
||||
delete newDevice.name;
|
||||
}
|
||||
this._device = newDevice;
|
||||
}
|
||||
|
||||
private _parentSelected(ev) {
|
||||
const newDevice = {
|
||||
...this._device!,
|
||||
included_in_stat: ev.target!.value,
|
||||
} as DeviceConsumptionEnergyPreference;
|
||||
if (!newDevice.included_in_stat) {
|
||||
delete newDevice.included_in_stat;
|
||||
}
|
||||
this._device = newDevice;
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
try {
|
||||
await this._params!.saveCallback(this._device!);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-statistic-picker {
|
||||
width: 100%;
|
||||
}
|
||||
ha-select {
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-textfield {
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-energy-device-settings-water": DialogEnergyDeviceSettingsWater;
|
||||
}
|
||||
}
|
||||
@@ -83,13 +83,6 @@ export interface EnergySettingsDeviceDialogParams {
|
||||
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface EnergySettingsDeviceWaterDialogParams {
|
||||
device?: DeviceConsumptionEnergyPreference;
|
||||
device_consumptions: DeviceConsumptionEnergyPreference[];
|
||||
statsMetadata?: Record<string, StatisticsMetaData>;
|
||||
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
|
||||
}
|
||||
|
||||
export const showEnergySettingsDeviceDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: EnergySettingsDeviceDialogParams
|
||||
@@ -167,17 +160,6 @@ export const showEnergySettingsGridFlowToDialog = (
|
||||
});
|
||||
};
|
||||
|
||||
export const showEnergySettingsDeviceWaterDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: EnergySettingsDeviceWaterDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-energy-device-settings-water",
|
||||
dialogImport: () => import("./dialog-energy-device-settings-water"),
|
||||
dialogParams: dialogParams,
|
||||
});
|
||||
};
|
||||
|
||||
export const showEnergySettingsGridPowerDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: EnergySettingsGridPowerDialogParams
|
||||
|
||||
@@ -22,7 +22,6 @@ import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import "../../../components/ha-alert";
|
||||
import "./components/ha-energy-device-settings";
|
||||
import "./components/ha-energy-device-settings-water";
|
||||
import "./components/ha-energy-grid-settings";
|
||||
import "./components/ha-energy-solar-settings";
|
||||
import "./components/ha-energy-battery-settings";
|
||||
@@ -33,7 +32,6 @@ import { fileDownload } from "../../../util/file_download";
|
||||
const INITIAL_CONFIG: EnergyPreferences = {
|
||||
energy_sources: [],
|
||||
device_consumption: [],
|
||||
device_consumption_water: [],
|
||||
};
|
||||
|
||||
@customElement("ha-config-energy")
|
||||
@@ -144,13 +142,6 @@ class HaConfigEnergy extends LitElement {
|
||||
.validationResult=${this._validationResult}
|
||||
@value-changed=${this._prefsChanged}
|
||||
></ha-energy-device-settings>
|
||||
<ha-energy-device-settings-water
|
||||
.hass=${this.hass}
|
||||
.preferences=${this._preferences!}
|
||||
.statsMetadata=${this._statsMetadata}
|
||||
.validationResult=${this._validationResult}
|
||||
@value-changed=${this._prefsChanged}
|
||||
></ha-energy-device-settings-water>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
|
||||
@@ -30,7 +30,6 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
|
||||
@state() private _preferences: EnergyPreferences = {
|
||||
energy_sources: [],
|
||||
device_consumption: [],
|
||||
device_consumption_water: [],
|
||||
};
|
||||
|
||||
public getCardSize() {
|
||||
|
||||
@@ -203,7 +203,7 @@ function formatTooltip(
|
||||
countNegative++;
|
||||
}
|
||||
}
|
||||
return `${param.marker} ${filterXSS(param.seriesName!)}: <div style="direction:ltr; display: inline;">${value} ${unit}</div>`;
|
||||
return `${param.marker} ${filterXSS(param.seriesName!)}: ${value} ${unit}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
let footer = "";
|
||||
|
||||
@@ -185,7 +185,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
this.hass.locale,
|
||||
params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined
|
||||
)} kWh`;
|
||||
return `${title}${params.marker} ${params.seriesName}: <div style="direction:ltr; display: inline;">${value}</div>`;
|
||||
return `${title}${params.marker} ${params.seriesName}: ${value}`;
|
||||
}
|
||||
|
||||
private _createOptions = memoizeOne(
|
||||
|
||||
@@ -402,9 +402,7 @@ class HuiEnergySankeyCard
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)}
|
||||
kWh</div>`;
|
||||
`${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)} kWh`;
|
||||
|
||||
protected _groupByFloorAndArea(deviceNodes: Node[]) {
|
||||
const areas: Record<string, { value: number; devices: Node[] }> = {
|
||||
|
||||
@@ -231,14 +231,6 @@ export interface EnergySankeyCardConfig extends EnergyCardBaseConfig {
|
||||
group_by_area?: boolean;
|
||||
}
|
||||
|
||||
export interface WaterSankeyCardConfig extends EnergyCardBaseConfig {
|
||||
type: "water-sankey";
|
||||
title?: string;
|
||||
layout?: "vertical" | "horizontal" | "auto";
|
||||
group_by_floor?: boolean;
|
||||
group_by_area?: boolean;
|
||||
}
|
||||
|
||||
export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig {
|
||||
type: "power-sources-graph";
|
||||
title?: string;
|
||||
|
||||
@@ -1,464 +0,0 @@
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import { getEnergyDataCollection } from "../../../../data/energy";
|
||||
import {
|
||||
calculateStatisticSumGrowth,
|
||||
getStatisticLabel,
|
||||
} from "../../../../data/recorder";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../../types";
|
||||
import type { WaterSankeyCardConfig } from "../types";
|
||||
import "../../../../components/chart/ha-sankey-chart";
|
||||
import type { Link, Node } from "../../../../components/chart/ha-sankey-chart";
|
||||
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
|
||||
import { MobileAwareMixin } from "../../../../mixins/mobile-aware-mixin";
|
||||
|
||||
const DEFAULT_CONFIG: Partial<WaterSankeyCardConfig> = {
|
||||
group_by_floor: true,
|
||||
group_by_area: true,
|
||||
};
|
||||
|
||||
@customElement("hui-water-sankey-card")
|
||||
class HuiWaterSankeyCard
|
||||
extends SubscribeMixin(MobileAwareMixin(LitElement))
|
||||
implements LovelaceCard
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public layout?: string;
|
||||
|
||||
@state() private _config?: WaterSankeyCardConfig;
|
||||
|
||||
@state() private _data?: EnergyData;
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public setConfig(config: WaterSankeyCardConfig): void {
|
||||
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
getEnergyDataCollection(this.hass, {
|
||||
key: this._config?.collection_key,
|
||||
}).subscribe((data) => {
|
||||
this._data = data;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public getCardSize(): Promise<number> | number {
|
||||
return 5;
|
||||
}
|
||||
|
||||
getGridOptions(): LovelaceGridOptions {
|
||||
return {
|
||||
columns: 12,
|
||||
min_columns: 6,
|
||||
rows: 6,
|
||||
min_rows: 2,
|
||||
};
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return (
|
||||
changedProps.has("_config") ||
|
||||
changedProps.has("_data") ||
|
||||
changedProps.has("_isMobileSize")
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this._data) {
|
||||
return html`${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.loading"
|
||||
)}`;
|
||||
}
|
||||
|
||||
const prefs = this._data.prefs;
|
||||
const waterSources = prefs.energy_sources.filter(
|
||||
(source) => source.type === "water"
|
||||
);
|
||||
|
||||
const computedStyle = getComputedStyle(this);
|
||||
|
||||
const nodes: Node[] = [];
|
||||
const links: Link[] = [];
|
||||
|
||||
// Calculate total water consumption from all devices
|
||||
let totalWaterConsumption = 0;
|
||||
prefs.device_consumption_water.forEach((device) => {
|
||||
const value =
|
||||
device.stat_consumption in this._data!.stats
|
||||
? calculateStatisticSumGrowth(
|
||||
this._data!.stats[device.stat_consumption]
|
||||
) || 0
|
||||
: 0;
|
||||
totalWaterConsumption += value;
|
||||
});
|
||||
|
||||
// Create home/consumption node
|
||||
const homeNode: Node = {
|
||||
id: "home",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.home"
|
||||
),
|
||||
value: Math.max(0, totalWaterConsumption),
|
||||
color: computedStyle.getPropertyValue("--primary-color").trim(),
|
||||
index: 1,
|
||||
};
|
||||
nodes.push(homeNode);
|
||||
|
||||
// Add water source nodes
|
||||
const waterColor = computedStyle
|
||||
.getPropertyValue("--energy-water-color")
|
||||
.trim();
|
||||
waterSources.forEach((source) => {
|
||||
if (source.type !== "water") {
|
||||
return;
|
||||
}
|
||||
const value =
|
||||
source.stat_energy_from in this._data!.stats
|
||||
? calculateStatisticSumGrowth(
|
||||
this._data!.stats[source.stat_energy_from]
|
||||
) || 0
|
||||
: 0;
|
||||
|
||||
if (value < 0.01) {
|
||||
return;
|
||||
}
|
||||
|
||||
nodes.push({
|
||||
id: source.stat_energy_from,
|
||||
label: getStatisticLabel(
|
||||
this.hass,
|
||||
source.stat_energy_from,
|
||||
this._data!.statsMetadata[source.stat_energy_from]
|
||||
),
|
||||
value,
|
||||
color: waterColor,
|
||||
index: 0,
|
||||
});
|
||||
|
||||
links.push({
|
||||
source: source.stat_energy_from,
|
||||
target: "home",
|
||||
value,
|
||||
});
|
||||
});
|
||||
|
||||
let untrackedConsumption = homeNode.value;
|
||||
const deviceNodes: Node[] = [];
|
||||
const parentLinks: Record<string, string> = {};
|
||||
prefs.device_consumption_water.forEach((device, idx) => {
|
||||
const value =
|
||||
device.stat_consumption in this._data!.stats
|
||||
? calculateStatisticSumGrowth(
|
||||
this._data!.stats[device.stat_consumption]
|
||||
) || 0
|
||||
: 0;
|
||||
if (value < 0.01) {
|
||||
return;
|
||||
}
|
||||
const node = {
|
||||
id: device.stat_consumption,
|
||||
label:
|
||||
device.name ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
device.stat_consumption,
|
||||
this._data!.statsMetadata[device.stat_consumption]
|
||||
),
|
||||
value,
|
||||
color: getGraphColorByIndex(idx, computedStyle),
|
||||
index: 4,
|
||||
parent: device.included_in_stat,
|
||||
};
|
||||
if (node.parent) {
|
||||
parentLinks[node.id] = node.parent;
|
||||
links.push({
|
||||
source: node.parent,
|
||||
target: node.id,
|
||||
});
|
||||
} else {
|
||||
untrackedConsumption -= value;
|
||||
}
|
||||
deviceNodes.push(node);
|
||||
});
|
||||
const devicesWithoutParent = deviceNodes.filter(
|
||||
(node) => !parentLinks[node.id]
|
||||
);
|
||||
|
||||
const { group_by_area, group_by_floor } = this._config;
|
||||
if (group_by_area || group_by_floor) {
|
||||
const { areas, floors } = this._groupByFloorAndArea(devicesWithoutParent);
|
||||
|
||||
Object.keys(floors)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(this.hass.floors[b]?.level ?? -Infinity) -
|
||||
(this.hass.floors[a]?.level ?? -Infinity)
|
||||
)
|
||||
.forEach((floorId) => {
|
||||
let floorNodeId = `floor_${floorId}`;
|
||||
if (floorId === "no_floor" || !group_by_floor) {
|
||||
// link "no_floor" areas to home
|
||||
floorNodeId = "home";
|
||||
} else {
|
||||
nodes.push({
|
||||
id: floorNodeId,
|
||||
label: this.hass.floors[floorId].name,
|
||||
value: floors[floorId].value,
|
||||
index: 2,
|
||||
color: computedStyle.getPropertyValue("--primary-color").trim(),
|
||||
});
|
||||
links.push({
|
||||
source: "home",
|
||||
target: floorNodeId,
|
||||
});
|
||||
}
|
||||
floors[floorId].areas.forEach((areaId) => {
|
||||
let targetNodeId: string;
|
||||
|
||||
if (areaId === "no_area" || !group_by_area) {
|
||||
// If group_by_area is false, link devices to floor or home
|
||||
targetNodeId = floorNodeId;
|
||||
} else {
|
||||
// Create area node and link it to floor
|
||||
const areaNodeId = `area_${areaId}`;
|
||||
nodes.push({
|
||||
id: areaNodeId,
|
||||
label: this.hass.areas[areaId]!.name,
|
||||
value: areas[areaId].value,
|
||||
index: 3,
|
||||
color: computedStyle.getPropertyValue("--primary-color").trim(),
|
||||
});
|
||||
links.push({
|
||||
source: floorNodeId,
|
||||
target: areaNodeId,
|
||||
value: areas[areaId].value,
|
||||
});
|
||||
targetNodeId = areaNodeId;
|
||||
}
|
||||
|
||||
// Link devices to the appropriate target (area, floor, or home)
|
||||
areas[areaId].devices.forEach((device) => {
|
||||
links.push({
|
||||
source: targetNodeId,
|
||||
target: device.id,
|
||||
value: device.value,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
devicesWithoutParent.forEach((deviceNode) => {
|
||||
links.push({
|
||||
source: "home",
|
||||
target: deviceNode.id,
|
||||
value: deviceNode.value,
|
||||
});
|
||||
});
|
||||
}
|
||||
const deviceSections = this._getDeviceSections(parentLinks, deviceNodes);
|
||||
deviceSections.forEach((section, index) => {
|
||||
section.forEach((node: Node) => {
|
||||
nodes.push({ ...node, index: 4 + index });
|
||||
});
|
||||
});
|
||||
|
||||
// untracked consumption
|
||||
if (untrackedConsumption > 0) {
|
||||
nodes.push({
|
||||
id: "untracked",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_devices_detail_graph.untracked_consumption"
|
||||
),
|
||||
value: untrackedConsumption,
|
||||
color: computedStyle
|
||||
.getPropertyValue("--state-unavailable-color")
|
||||
.trim(),
|
||||
index: 3 + deviceSections.length,
|
||||
});
|
||||
links.push({
|
||||
source: "home",
|
||||
target: "untracked",
|
||||
value: untrackedConsumption,
|
||||
});
|
||||
}
|
||||
|
||||
const hasData = nodes.some((node) => node.value > 0);
|
||||
|
||||
const vertical =
|
||||
this._config.layout === "vertical" ||
|
||||
(this._config.layout !== "horizontal" && this._isMobileSize);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this._config.title}
|
||||
class=${classMap({
|
||||
"is-grid": this.layout === "grid",
|
||||
"is-panel": this.layout === "panel",
|
||||
"is-vertical": vertical,
|
||||
})}
|
||||
>
|
||||
<div class="card-content">
|
||||
${hasData
|
||||
? html`<ha-sankey-chart
|
||||
.data=${{ nodes, links }}
|
||||
.vertical=${vertical}
|
||||
.valueFormatter=${this._valueFormatter}
|
||||
></ha-sankey-chart>`
|
||||
: html`${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.no_data_period"
|
||||
)}`}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)} ${this._data!.waterUnit}`;
|
||||
|
||||
protected _groupByFloorAndArea(deviceNodes: Node[]) {
|
||||
const areas: Record<string, { value: number; devices: Node[] }> = {
|
||||
no_area: {
|
||||
value: 0,
|
||||
devices: [],
|
||||
},
|
||||
};
|
||||
const floors: Record<string, { value: number; areas: string[] }> = {
|
||||
no_floor: {
|
||||
value: 0,
|
||||
areas: ["no_area"],
|
||||
},
|
||||
};
|
||||
deviceNodes.forEach((deviceNode) => {
|
||||
const entity = this.hass.states[deviceNode.id];
|
||||
const { area, floor } = entity
|
||||
? getEntityContext(
|
||||
entity,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: { area: null, floor: null };
|
||||
if (area) {
|
||||
if (area.area_id in areas) {
|
||||
areas[area.area_id].value += deviceNode.value;
|
||||
areas[area.area_id].devices.push(deviceNode);
|
||||
} else {
|
||||
areas[area.area_id] = {
|
||||
value: deviceNode.value,
|
||||
devices: [deviceNode],
|
||||
};
|
||||
}
|
||||
// see if the area has a floor
|
||||
if (floor) {
|
||||
if (floor.floor_id in floors) {
|
||||
floors[floor.floor_id].value += deviceNode.value;
|
||||
if (!floors[floor.floor_id].areas.includes(area.area_id)) {
|
||||
floors[floor.floor_id].areas.push(area.area_id);
|
||||
}
|
||||
} else {
|
||||
floors[floor.floor_id] = {
|
||||
value: deviceNode.value,
|
||||
areas: [area.area_id],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
floors.no_floor.value += deviceNode.value;
|
||||
if (!floors.no_floor.areas.includes(area.area_id)) {
|
||||
floors.no_floor.areas.unshift(area.area_id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
areas.no_area.value += deviceNode.value;
|
||||
areas.no_area.devices.push(deviceNode);
|
||||
}
|
||||
});
|
||||
return { areas, floors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Organizes device nodes into hierarchical sections based on parent-child relationships.
|
||||
*/
|
||||
protected _getDeviceSections(
|
||||
parentLinks: Record<string, string>,
|
||||
deviceNodes: Node[]
|
||||
): Node[][] {
|
||||
const parentSection: Node[] = [];
|
||||
const childSection: Node[] = [];
|
||||
const parentIds = Object.values(parentLinks);
|
||||
const remainingLinks: typeof parentLinks = {};
|
||||
|
||||
deviceNodes.forEach((deviceNode) => {
|
||||
const isChild = deviceNode.id in parentLinks;
|
||||
const isParent = parentIds.includes(deviceNode.id);
|
||||
if (isParent && !isChild) {
|
||||
// Top-level parents (have children but no parents themselves)
|
||||
parentSection.push(deviceNode);
|
||||
} else {
|
||||
childSection.push(deviceNode);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter out links where parent is already in current parent section
|
||||
Object.entries(parentLinks).forEach(([child, parent]) => {
|
||||
if (!parentSection.some((node) => node.id === parent)) {
|
||||
remainingLinks[child] = parent;
|
||||
}
|
||||
});
|
||||
|
||||
if (parentSection.length > 0) {
|
||||
// Recursively process child section with remaining links
|
||||
return [
|
||||
parentSection,
|
||||
...this._getDeviceSections(remainingLinks, childSection),
|
||||
];
|
||||
}
|
||||
|
||||
// Base case: no more parent-child relationships to process
|
||||
return [deviceNodes];
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--chart-max-height: none;
|
||||
}
|
||||
ha-card.is-vertical {
|
||||
height: 500px;
|
||||
}
|
||||
ha-card.is-grid,
|
||||
ha-card.is-panel {
|
||||
height: 100%;
|
||||
}
|
||||
.card-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-water-sankey-card": HuiWaterSankeyCard;
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,6 @@ const LAZY_LOAD_TYPES = {
|
||||
"energy-usage-graph": () =>
|
||||
import("../cards/energy/hui-energy-usage-graph-card"),
|
||||
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
|
||||
"water-sankey": () => import("../cards/water/hui-water-sankey-card"),
|
||||
"power-sources-graph": () =>
|
||||
import("../cards/energy/hui-power-sources-graph-card"),
|
||||
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
||||
|
||||
@@ -3225,22 +3225,6 @@
|
||||
"included_in_device_helper": "If this device is already counted by another device (such as a smart switch measured by a smart breaker), selecting the upstream device prevents duplicate energy tracking.",
|
||||
"no_upstream_devices": "No eligible upstream devices"
|
||||
}
|
||||
},
|
||||
"device_consumption_water": {
|
||||
"title": "Individual water devices",
|
||||
"sub": "Tracking the water usage of individual devices allows Home Assistant to break down your water usage by device.",
|
||||
"learn_more": "More information on how to get started.",
|
||||
"devices": "Devices",
|
||||
"add_device": "Add device",
|
||||
"dialog": {
|
||||
"header": "Add a water device",
|
||||
"display_name": "Display name",
|
||||
"device_consumption_water": "Device water consumption",
|
||||
"selected_stat_intro": "Select the water sensor that measures the device's water usage in either of {unit}.",
|
||||
"included_in_device": "Upstream device",
|
||||
"included_in_device_helper": "If this device is already counted by another device (such as a water meter measured by the main water supply), selecting the upstream device prevents duplicate water tracking.",
|
||||
"no_upstream_devices": "No eligible upstream devices"
|
||||
}
|
||||
}
|
||||
},
|
||||
"helpers": {
|
||||
@@ -6776,6 +6760,7 @@
|
||||
},
|
||||
"analytics": {
|
||||
"caption": "Analytics",
|
||||
"header": "Home Assistant analytics",
|
||||
"description": "Learn how to share data to improve Home Assistant",
|
||||
"preferences": {
|
||||
"base": {
|
||||
@@ -6793,10 +6778,17 @@
|
||||
"diagnostics": {
|
||||
"title": "Diagnostics",
|
||||
"description": "Share crash reports when unexpected errors occur."
|
||||
},
|
||||
"snapshots": {
|
||||
"title": "Devices",
|
||||
"description": "Generic information about your devices.",
|
||||
"header": "Device analytics",
|
||||
"info": "Anonymously share data about your devices to help build the Open Home Foundation’s device database. This free, open source resource helps users find useful information about smart home devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign).",
|
||||
"learn_more": "Learn more about the device database and how we process your data"
|
||||
}
|
||||
},
|
||||
"need_base_enabled": "You need to enable basic analytics for this option to be available",
|
||||
"learn_more": "How we process your data",
|
||||
"learn_more": "Learn how we process your data",
|
||||
"intro": "Share anonymized information from your installation to help make Home Assistant better and help us convince manufacturers to add local control and privacy-focused features.",
|
||||
"download_device_info": "Preview device analytics"
|
||||
},
|
||||
@@ -9466,8 +9458,7 @@
|
||||
"energy_sources_table_title": "Sources",
|
||||
"energy_devices_graph_title": "Individual devices total usage",
|
||||
"energy_devices_detail_graph_title": "Individual devices detail usage",
|
||||
"energy_sankey_title": "Energy flow",
|
||||
"water_sankey_title": "Water flow"
|
||||
"energy_sankey_title": "Energy flow"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -9337,7 +9337,7 @@ __metadata:
|
||||
husky: "npm:9.1.7"
|
||||
idb-keyval: "npm:6.2.2"
|
||||
intl-messageformat: "npm:10.7.18"
|
||||
js-yaml: "npm:4.1.1"
|
||||
js-yaml: "npm:4.1.0"
|
||||
jsdom: "npm:27.1.0"
|
||||
jszip: "npm:3.10.1"
|
||||
leaflet: "npm:1.9.4"
|
||||
@@ -10407,14 +10407,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-yaml@npm:4.1.1, js-yaml@npm:^4.1.0":
|
||||
version: 4.1.1
|
||||
resolution: "js-yaml@npm:4.1.1"
|
||||
"js-yaml@npm:4.1.0, js-yaml@npm:^4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "js-yaml@npm:4.1.0"
|
||||
dependencies:
|
||||
argparse: "npm:^2.0.1"
|
||||
bin:
|
||||
js-yaml: bin/js-yaml.js
|
||||
checksum: 10/a52d0519f0f4ef5b4adc1cde466cb54c50d56e2b4a983b9d5c9c0f2f99462047007a6274d7e95617a21d3c91fde3ee6115536ed70991cd645ba8521058b78f77
|
||||
checksum: 10/c138a34a3fd0d08ebaf71273ad4465569a483b8a639e0b118ff65698d257c2791d3199e3f303631f2cb98213fa7b5f5d6a4621fd0fff819421b990d30d967140
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user