mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-02 22:41:47 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ae9bb996f |
+1
-1
@@ -180,7 +180,7 @@
|
||||
"jsdom": "29.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "5.0.1",
|
||||
"lint-staged": "17.0.6",
|
||||
"lint-staged": "17.0.5",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.18.1",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-tooltip";
|
||||
|
||||
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
|
||||
@@ -12,6 +13,7 @@ export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
*
|
||||
* @attr {"pass"|"fail"|"invalid"|"unknown"} state - The current live-test state. Defaults to `unknown`.
|
||||
* @attr {string} label - Accessible label announced by assistive technology.
|
||||
* @attr {string} message - Optional tooltip body shown on hover/focus.
|
||||
*/
|
||||
@customElement("ha-automation-row-live-test")
|
||||
export class HaAutomationRowLiveTest extends LitElement {
|
||||
@@ -19,6 +21,8 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
@property() public message?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -27,38 +31,39 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
tabindex="0"
|
||||
aria-label=${this.label}
|
||||
></div>
|
||||
${this.message
|
||||
? html`<ha-tooltip for="indicator">${this.message}</ha-tooltip>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
inset-inline-end: -6px;
|
||||
display: inline-block;
|
||||
}
|
||||
#indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
border: var(--ha-border-width-md) solid;
|
||||
border: 3px solid;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--card-background-color);
|
||||
box-shadow: 0 0 0 2px var(--card-background-color);
|
||||
transition: all var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
:host([state="pass"]) #indicator {
|
||||
background-color: var(--ha-color-green-60);
|
||||
border-color: var(--ha-color-green-60);
|
||||
background-color: var(--ha-color-fill-success-loud-resting);
|
||||
border-color: var(--ha-color-fill-success-loud-resting);
|
||||
}
|
||||
:host([state="fail"]) #indicator {
|
||||
border-color: var(--ha-color-orange-60);
|
||||
border-color: var(--ha-color-fill-warning-loud-resting);
|
||||
}
|
||||
:host([state="invalid"]) #indicator {
|
||||
border-color: var(--ha-color-red-60);
|
||||
border-color: var(--ha-color-fill-danger-loud-resting);
|
||||
}
|
||||
:host([state="unknown"]) #indicator {
|
||||
border-color: var(--ha-color-neutral-60);
|
||||
border-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ export class HaAutomationRow extends LitElement {
|
||||
::slotted([slot="leading-icon"]) {
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
}
|
||||
:host([building-block]) ::slotted(#condition-icon) {
|
||||
:host([building-block]) ::slotted([slot="leading-icon"]) {
|
||||
--mdc-icon-size: var(--ha-space-5);
|
||||
color: var(--white-color);
|
||||
transform: rotate(-45deg);
|
||||
|
||||
@@ -101,22 +101,18 @@ export class HaSankeyChart extends LitElement {
|
||||
const value = this.valueFormatter
|
||||
? this.valueFormatter(data.value)
|
||||
: data.value;
|
||||
// Keep numbers and units left-to-right, even in RTL locales.
|
||||
const formattedValue = html`<div style="direction:ltr; display: inline;">
|
||||
${value}
|
||||
</div>`;
|
||||
if (data.id) {
|
||||
const node = this.data.nodes.find((n) => n.id === data.id);
|
||||
return html`<ha-chart-tooltip-marker
|
||||
.color=${String(params.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${node?.label ?? data.id}<br />${formattedValue}`;
|
||||
${node?.label ?? data.id}<br />${value}`;
|
||||
}
|
||||
if (data.source && data.target) {
|
||||
const source = this.data.nodes.find((n) => n.id === data.source);
|
||||
const target = this.data.nodes.find((n) => n.id === data.target);
|
||||
return html`${source?.label ?? data.source} →
|
||||
${target?.label ?? data.target}<br />${formattedValue}`;
|
||||
${target?.label ?? data.target}<br />${value}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -485,17 +485,6 @@ export const migrateAutomationTrigger = (
|
||||
}
|
||||
delete trigger.platform;
|
||||
}
|
||||
|
||||
if ("options" in trigger) {
|
||||
if (trigger.options && "behavior" in trigger.options) {
|
||||
if (trigger.options.behavior === "any") {
|
||||
trigger.options.behavior = "each";
|
||||
} else if (trigger.options.behavior === "last") {
|
||||
trigger.options.behavior = "all";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return trigger;
|
||||
};
|
||||
|
||||
|
||||
@@ -26,6 +26,13 @@ export interface HomeFrontendSystemData {
|
||||
shortcuts?: ShortcutItem[];
|
||||
}
|
||||
|
||||
export interface EnergyFrontendSystemData {
|
||||
// Stable "<view>.<card-type>" keys of energy dashboard cards the user has
|
||||
// hidden. An absent key or array means nothing is hidden (all cards visible),
|
||||
// so cards added in the future are shown by default.
|
||||
hidden_cards?: string[];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface FrontendUserData {
|
||||
core: CoreFrontendUserData;
|
||||
@@ -34,6 +41,7 @@ declare global {
|
||||
interface FrontendSystemData {
|
||||
core: CoreFrontendSystemData;
|
||||
home: HomeFrontendSystemData;
|
||||
energy: EnergyFrontendSystemData;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,8 +139,6 @@ export class HassTabsSubpage extends LitElement {
|
||||
);
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues<this>) {
|
||||
this.toggleAttribute("narrow", this._narrow);
|
||||
|
||||
if (changedProperties.has("route")) {
|
||||
const currentPath = `${this.route.prefix}${this.route.path}`;
|
||||
this._activeTab = this.tabs.find((tab) =>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "@home-assistant/webawesome/dist/components/tag/tag";
|
||||
import { mdiCheckCircle, mdiHelpCircleOutline } from "@mdi/js";
|
||||
import { mdiHelpCircleOutline } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -25,9 +25,7 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
|
||||
@property() public stage: AddonStage = "stable";
|
||||
|
||||
@property() public state?: AddonState;
|
||||
|
||||
@property({ type: Boolean }) public installed = false;
|
||||
@property() public state: AddonState = null;
|
||||
|
||||
@property() public description?: string;
|
||||
|
||||
@@ -79,23 +77,13 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${this.tags?.length || this.state !== undefined || this.installed
|
||||
${this.tags?.length || this.state
|
||||
? html`
|
||||
<div class="footer">
|
||||
${this.state !== undefined
|
||||
? html`<supervisor-apps-state
|
||||
.state=${this.state || "unknown"}
|
||||
></supervisor-apps-state>`
|
||||
: this.installed
|
||||
? html`<div class="installed">
|
||||
<ha-svg-icon .path=${mdiCheckCircle}></ha-svg-icon>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.apps.state.installed"
|
||||
)}</span
|
||||
>
|
||||
</div>`
|
||||
: html`<span></span>`}
|
||||
<supervisor-apps-state
|
||||
.state=${this.state || "unknown"}
|
||||
></supervisor-apps-state>
|
||||
|
||||
${this.tags?.length
|
||||
? html`<div class="tags">
|
||||
${this.tags.map(
|
||||
@@ -171,17 +159,6 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
.installed {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
color: var(--ha-color-text-secondary);
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
.installed ha-svg-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
color: var(--ha-color-on-success-normal);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import {
|
||||
mdiAlertDecagramOutline,
|
||||
mdiArrowUpBoldCircle,
|
||||
mdiArrowUpBoldCircleOutline,
|
||||
mdiFlask,
|
||||
mdiPuzzle,
|
||||
} from "@mdi/js";
|
||||
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
@@ -17,7 +10,6 @@ import type { HassioAddonRepository } from "../../../data/hassio/addon";
|
||||
import type { StoreAddon } from "../../../data/supervisor/store";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./components/supervisor-apps-card-content";
|
||||
import type { AppTag } from "./components/supervisor-apps-card-content";
|
||||
import { filterAndSort } from "./components/supervisor-apps-filter";
|
||||
import { supervisorAppsStyle } from "./resources/supervisor-apps-style";
|
||||
|
||||
@@ -62,29 +54,21 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
<div class="content">
|
||||
<h1>${repo.name}</h1>
|
||||
<div class="card-group">
|
||||
${addons.map((addon) => {
|
||||
const tags = this._getAppTags(addon);
|
||||
return html`
|
||||
${addons.map(
|
||||
(addon) => html`
|
||||
<ha-card
|
||||
outlined
|
||||
.addon=${addon}
|
||||
class=${addon.available ? "" : "not_available"}
|
||||
@click=${this._addonTapped}
|
||||
>
|
||||
<div
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
"has-footer": tags.length > 0 || addon.installed,
|
||||
})}
|
||||
>
|
||||
<div class="card-content">
|
||||
<supervisor-apps-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${addon.name}
|
||||
.stage=${addon.stage}
|
||||
.description=${addon.description}
|
||||
.available=${addon.available}
|
||||
.installed=${addon.installed}
|
||||
.tags=${tags}
|
||||
.icon=${addon.installed && addon.update_available
|
||||
? mdiArrowUpBoldCircle
|
||||
: mdiPuzzle}
|
||||
@@ -124,8 +108,8 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
></supervisor-apps-card-content>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
})}
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -135,32 +119,6 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
navigate(`/config/app/${ev.currentTarget.addon.slug}/info?store=true`);
|
||||
}
|
||||
|
||||
private _getAppTags(addon: StoreAddon): AppTag[] {
|
||||
const labels: AppTag[] = [];
|
||||
|
||||
if (addon.installed && addon.update_available) {
|
||||
labels.push({
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.apps.state.update_available`
|
||||
),
|
||||
variant: "brand",
|
||||
iconPath: mdiArrowUpBoldCircleOutline,
|
||||
});
|
||||
}
|
||||
if (addon.stage !== "stable") {
|
||||
labels.push({
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.apps.dashboard.capability.stages.${addon.stage}`
|
||||
),
|
||||
variant: addon.stage === "experimental" ? "warning" : "danger",
|
||||
iconPath:
|
||||
addon.stage === "experimental" ? mdiFlask : mdiAlertDecagramOutline,
|
||||
});
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
supervisorAppsStyle,
|
||||
@@ -169,9 +127,6 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-content.has-footer {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
|
||||
}
|
||||
.not_available {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
@@ -212,27 +211,11 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
);
|
||||
|
||||
return html`
|
||||
<div id="condition-icon" class="icon-badge-wrapper" slot="leading-icon">
|
||||
<ha-condition-icon
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition.condition}
|
||||
></ha-condition-icon>
|
||||
${this.optionsInSidebar && this.condition.condition !== "trigger"
|
||||
? html`<ha-automation-row-live-test
|
||||
.state=${this._liveTestResult.state}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult.state}`
|
||||
)}
|
||||
></ha-automation-row-live-test>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this.optionsInSidebar &&
|
||||
this.condition.condition !== "trigger" &&
|
||||
this._liveTestResult.message
|
||||
? html`<ha-tooltip for="condition-icon" slot="leading-icon"
|
||||
>${this._liveTestResult.message}</ha-tooltip
|
||||
>`
|
||||
: nothing}
|
||||
<ha-condition-icon
|
||||
slot="leading-icon"
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition.condition}
|
||||
></ha-condition-icon>
|
||||
<h3 slot="header">
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
@@ -548,7 +531,17 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
@click=${this._toggleSidebar}
|
||||
@toggle-collapsed=${this._toggleCollapse}
|
||||
>${this._renderRow()}
|
||||
</ha-automation-row>`
|
||||
<ha-automation-row-live-test
|
||||
slot="icons"
|
||||
.state=${this.condition.condition !== "trigger"
|
||||
? this._liveTestResult.state
|
||||
: "unknown"}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.live_test_state.${this.condition.condition !== "trigger" ? this._liveTestResult.state : "unknown"}`
|
||||
)}
|
||||
.message=${this._liveTestResult.message}
|
||||
></ha-automation-row-live-test
|
||||
></ha-automation-row>`
|
||||
: html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
|
||||
@@ -53,11 +53,6 @@ export const rowStyles = css`
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.icon-badge-wrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.note-indicator {
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
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 type { LocalizeKeys } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-settings-row";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import {
|
||||
fetchFrontendSystemData,
|
||||
saveFrontendSystemData,
|
||||
} from "../../../../data/frontend";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import type {
|
||||
EnergyCardCatalogEntry,
|
||||
EnergyViewPath,
|
||||
} from "../../../energy/strategies/energy-cards";
|
||||
import { ENERGY_CARD_CATALOG } from "../../../energy/strategies/energy-cards";
|
||||
import type { EnergyCustomiseDialogParams } from "./show-dialog-energy-customise";
|
||||
|
||||
const VIEW_GROUPS: { view: EnergyViewPath; labelKey: LocalizeKeys }[] = [
|
||||
{
|
||||
view: "overview",
|
||||
labelKey: "ui.panel.config.energy.customise.groups.overview",
|
||||
},
|
||||
{ view: "electricity", labelKey: "ui.panel.config.energy.tabs.electricity" },
|
||||
{ view: "gas", labelKey: "ui.panel.config.energy.tabs.gas" },
|
||||
{ view: "water", labelKey: "ui.panel.config.energy.tabs.water" },
|
||||
{ view: "now", labelKey: "ui.panel.config.energy.customise.groups.now" },
|
||||
];
|
||||
|
||||
@customElement("dialog-energy-customise")
|
||||
export class DialogEnergyCustomise
|
||||
extends LitElement
|
||||
implements HassDialog<EnergyCustomiseDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: EnergyCustomiseDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
// Working copy of the hidden card keys. A switch that is ON means the card is
|
||||
// visible, i.e. its key is NOT in this set.
|
||||
@state() private _hidden?: Set<string>;
|
||||
|
||||
public showDialog(params: EnergyCustomiseDialogParams): void {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
this._loadHidden();
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this._hidden = undefined;
|
||||
this._error = undefined;
|
||||
this._submitting = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private async _loadHidden(): Promise<void> {
|
||||
this._error = undefined;
|
||||
try {
|
||||
// The card labels reuse keys from the "energy" translation fragment,
|
||||
// which is not guaranteed to be loaded on the config page.
|
||||
const [data] = await Promise.all([
|
||||
fetchFrontendSystemData(this.hass.connection, "energy"),
|
||||
this.hass.loadFragmentTranslation("energy"),
|
||||
]);
|
||||
this._hidden = new Set(data?.hidden_cards ?? []);
|
||||
} catch (err: any) {
|
||||
this._error = err?.message || "Unknown error";
|
||||
this._hidden = new Set();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.headerTitle=${this.hass.localize(
|
||||
"ui.panel.config.energy.customise.title"
|
||||
)}
|
||||
.headerSubtitle=${this.hass.localize(
|
||||
"ui.panel.config.energy.customise.subtitle"
|
||||
)}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${!this._hidden
|
||||
? html`<div class="loading">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</div>`
|
||||
: this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: this._renderGroups()}
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${this._submitting || !this._hidden || !!this._error}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderGroups() {
|
||||
const prefs = this._params!.preferences;
|
||||
return VIEW_GROUPS.map((group) => {
|
||||
const cards = ENERGY_CARD_CATALOG.filter((c) => c.view === group.view);
|
||||
// Hide the whole group when none of its cards apply to the current config.
|
||||
if (!cards.some((c) => c.isApplicable(prefs))) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
outlined
|
||||
expanded
|
||||
.header=${this.hass.localize(group.labelKey)}
|
||||
>
|
||||
<div class="cards">
|
||||
${cards.map((card) => this._renderCardRow(card))}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
private _renderCardRow(card: EnergyCardCatalogEntry) {
|
||||
const applicable = card.isApplicable(this._params!.preferences);
|
||||
const label = this.hass.localize(card.labelKey);
|
||||
const rowId = `row-${card.key}`;
|
||||
return html`
|
||||
<ha-settings-row slim id=${rowId}>
|
||||
<span slot="heading" class=${applicable ? "" : "disabled"}
|
||||
>${label}</span
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${applicable && !this._hidden!.has(card.key)}
|
||||
.disabled=${!applicable}
|
||||
.ariaLabel=${label}
|
||||
data-card-key=${card.key}
|
||||
@change=${this._toggleCard}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
${applicable
|
||||
? nothing
|
||||
: html`
|
||||
<ha-tooltip .for=${rowId} placement="top">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.customise.unavailable"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggleCard = (ev: Event): void => {
|
||||
const target = ev.currentTarget as HaSwitch;
|
||||
const cardKey = target.dataset.cardKey;
|
||||
if (!cardKey) {
|
||||
return;
|
||||
}
|
||||
const next = new Set(this._hidden);
|
||||
if (target.checked) {
|
||||
next.delete(cardKey);
|
||||
} else {
|
||||
next.add(cardKey);
|
||||
}
|
||||
this._hidden = next;
|
||||
};
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
if (!this._hidden) {
|
||||
return;
|
||||
}
|
||||
this._submitting = true;
|
||||
try {
|
||||
const hidden = Array.from(this._hidden);
|
||||
await saveFrontendSystemData(this.hass.connection, "energy", {
|
||||
hidden_cards: hidden.length ? hidden : undefined,
|
||||
});
|
||||
this._params?.saveCallback?.();
|
||||
this.closeDialog();
|
||||
} catch (_err) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.energy.customise.save_failed"
|
||||
),
|
||||
duration: 0,
|
||||
dismissable: true,
|
||||
});
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: var(--ha-space-6);
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--ha-space-6);
|
||||
}
|
||||
span.disabled {
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
ha-expansion-panel {
|
||||
display: block;
|
||||
--expansion-panel-content-padding: 0;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
--ha-card-border-radius: var(--ha-border-radius-md);
|
||||
}
|
||||
ha-expansion-panel + ha-expansion-panel {
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
.cards {
|
||||
padding: 0 var(--ha-space-3);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-energy-customise": DialogEnergyCustomise;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { EnergyPreferences } from "../../../../data/energy";
|
||||
|
||||
export interface EnergyCustomiseDialogParams {
|
||||
preferences: EnergyPreferences;
|
||||
// Called after a successful save (e.g. to show a toast on the page).
|
||||
saveCallback?: () => void;
|
||||
}
|
||||
|
||||
export const loadEnergyCustomiseDialog = () =>
|
||||
import("./dialog-energy-customise");
|
||||
|
||||
export const showEnergyCustomiseDialog = (
|
||||
element: HTMLElement,
|
||||
params: EnergyCustomiseDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-energy-customise",
|
||||
dialogImport: loadEnergyCustomiseDialog,
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,11 @@
|
||||
import "../../../layouts/hass-error-screen";
|
||||
import { mdiDownload, mdiFire, mdiLightningBolt, mdiWater } from "@mdi/js";
|
||||
import {
|
||||
mdiDownload,
|
||||
mdiFire,
|
||||
mdiLightningBolt,
|
||||
mdiViewDashboardEdit,
|
||||
mdiWater,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -31,7 +37,9 @@ import "./components/ha-energy-battery-settings";
|
||||
import "./components/ha-energy-gas-settings";
|
||||
import "./components/ha-energy-water-settings";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import { showEnergyCustomiseDialog } from "./dialogs/show-dialog-energy-customise";
|
||||
|
||||
const INITIAL_CONFIG: EnergyPreferences = {
|
||||
energy_sources: [],
|
||||
@@ -124,14 +132,22 @@ class HaConfigEnergy extends LitElement {
|
||||
.route=${this.route}
|
||||
.tabs=${TABS}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.path=${mdiDownload}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.devices.download_diagnostics"
|
||||
)}
|
||||
@click=${this._downloadDiagnostics}
|
||||
></ha-icon-button>
|
||||
<div slot="toolbar-icon" class="toolbar-icons">
|
||||
<ha-icon-button
|
||||
.path=${mdiViewDashboardEdit}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.customise.toolbar_action"
|
||||
)}
|
||||
@click=${this._customise}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.path=${mdiDownload}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.devices.download_diagnostics"
|
||||
)}
|
||||
@click=${this._downloadDiagnostics}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-alert>
|
||||
${this.hass.localize("ui.panel.config.energy.new_device_info")}
|
||||
</ha-alert>
|
||||
@@ -254,6 +270,20 @@ class HaConfigEnergy extends LitElement {
|
||||
this._statsMetadata = statsMetadata;
|
||||
}
|
||||
|
||||
private _customise() {
|
||||
if (!this._preferences) {
|
||||
return;
|
||||
}
|
||||
showEnergyCustomiseDialog(this, {
|
||||
preferences: this._preferences,
|
||||
saveCallback: () => {
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.panel.config.energy.customise.saved"),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _downloadDiagnostics() {
|
||||
const data = {
|
||||
version: this.hass.config.version,
|
||||
@@ -285,6 +315,10 @@ class HaConfigEnergy extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.toolbar-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.content {
|
||||
padding: 0 var(--ha-space-5);
|
||||
max-width: 1040px;
|
||||
|
||||
+9
-12
@@ -25,7 +25,6 @@ import {
|
||||
type ExtEntityRegistryEntry,
|
||||
} from "../../../../../data/entity/entity_registry";
|
||||
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { OVERRIDE_DEVICE_CLASSES } from "../../../entities/entity-registry-settings-editor";
|
||||
import "./matter-add-device/matter-add-device-apple-home";
|
||||
import "./matter-add-device/matter-add-device-existing";
|
||||
import "./matter-add-device/matter-add-device-generic";
|
||||
@@ -140,17 +139,15 @@ class DialogMatterAddDevice extends LitElement {
|
||||
entityIds
|
||||
);
|
||||
|
||||
this._mainEntity = Object.values(entries).find((entry) => {
|
||||
if (entry.entity_category) return false;
|
||||
const domain = computeDomain(entry.entity_id);
|
||||
const deviceClasses = OVERRIDE_DEVICE_CLASSES[domain];
|
||||
if (!deviceClasses) return false;
|
||||
const deviceClass = entry.device_class ?? entry.original_device_class;
|
||||
if (!deviceClass) return false;
|
||||
return deviceClasses.some(
|
||||
(classes) => classes.length > 1 && classes.includes(deviceClass)
|
||||
);
|
||||
});
|
||||
const mainEntry = Object.values(entries).find(
|
||||
(e) => e.original_name === null
|
||||
);
|
||||
if (!mainEntry) return;
|
||||
|
||||
const domain = computeDomain(mainEntry.entity_id);
|
||||
if (domain === "cover" || domain === "binary_sensor") {
|
||||
this._mainEntity = mainEntry;
|
||||
}
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
|
||||
@@ -6,6 +6,8 @@ import "../../components/ha-alert";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import type { EnergyFrontendSystemData } from "../../data/frontend";
|
||||
import { fetchFrontendSystemData } from "../../data/frontend";
|
||||
import type { LovelaceConfig } from "../../data/lovelace/config/types";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo } from "../../types";
|
||||
@@ -26,6 +28,8 @@ class PanelEnergy extends LitElement {
|
||||
|
||||
@state() private _lovelace?: Lovelace;
|
||||
|
||||
@state() private _config: EnergyFrontendSystemData = {};
|
||||
|
||||
@property({ attribute: false }) public route?: {
|
||||
path: string;
|
||||
prefix: string;
|
||||
@@ -58,10 +62,23 @@ class PanelEnergy extends LitElement {
|
||||
await Promise.all([
|
||||
this.hass.loadFragmentTranslation("lovelace"),
|
||||
this.hass.loadFragmentTranslation("energy"),
|
||||
this._loadSystemData(),
|
||||
]);
|
||||
this._loadConfig();
|
||||
}
|
||||
|
||||
private async _loadSystemData() {
|
||||
try {
|
||||
const data = await fetchFrontendSystemData(
|
||||
this.hass.connection,
|
||||
"energy"
|
||||
);
|
||||
this._config = data || {};
|
||||
} catch (_err) {
|
||||
this._config = {};
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadConfig() {
|
||||
try {
|
||||
this._error = undefined;
|
||||
@@ -94,6 +111,7 @@ class PanelEnergy extends LitElement {
|
||||
this.route?.path === "/now"
|
||||
? DEFAULT_POWER_COLLECTION_KEY
|
||||
: undefined,
|
||||
hidden_cards: this._config.hidden_cards,
|
||||
},
|
||||
},
|
||||
this.hass
|
||||
@@ -164,7 +182,8 @@ class PanelEnergy extends LitElement {
|
||||
navigate(`/config/energy/${tab}?historyBack=1`);
|
||||
}
|
||||
|
||||
private _reloadConfig() {
|
||||
private async _reloadConfig() {
|
||||
await this._loadSystemData();
|
||||
this._loadConfig();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
import type { LocalizeKeys } from "../../../common/translations/localize";
|
||||
import type {
|
||||
EnergyPreferences,
|
||||
GridSourceTypeEnergyPreference,
|
||||
} from "../../../data/energy";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
|
||||
/** Strategy config shared by the per-view energy strategies. */
|
||||
export interface EnergyViewStrategyConfig extends LovelaceStrategyConfig {
|
||||
collection_key?: string;
|
||||
hidden_cards?: string[];
|
||||
}
|
||||
|
||||
export type EnergyViewPath =
|
||||
| "overview"
|
||||
| "electricity"
|
||||
| "gas"
|
||||
| "water"
|
||||
| "now";
|
||||
|
||||
// --- Applicability helpers -------------------------------------------------
|
||||
// These mirror, one-to-one, the conditions the individual view strategies use
|
||||
// to decide whether to emit a card. The catalog and the strategies must agree
|
||||
// on what "applicable" means, so the conditions live here and are reused.
|
||||
|
||||
export const hasGridSource = (prefs: EnergyPreferences): boolean =>
|
||||
prefs.energy_sources.some(
|
||||
(source): source is GridSourceTypeEnergyPreference =>
|
||||
source.type === "grid" &&
|
||||
(!!source.stat_energy_from || !!source.stat_energy_to)
|
||||
);
|
||||
|
||||
export const hasReturn = (prefs: EnergyPreferences): boolean =>
|
||||
prefs.energy_sources.some(
|
||||
(source) => source.type === "grid" && !!source.stat_energy_to
|
||||
);
|
||||
|
||||
export const hasSolar = (prefs: EnergyPreferences): boolean =>
|
||||
prefs.energy_sources.some((source) => source.type === "solar");
|
||||
|
||||
export const hasBattery = (prefs: EnergyPreferences): boolean =>
|
||||
prefs.energy_sources.some((source) => source.type === "battery");
|
||||
|
||||
export const hasGasSource = (prefs: EnergyPreferences): boolean =>
|
||||
prefs.energy_sources.some((source) => source.type === "gas");
|
||||
|
||||
export const hasWaterSource = (prefs: EnergyPreferences): boolean =>
|
||||
prefs.energy_sources.some((source) => source.type === "water");
|
||||
|
||||
export const hasWaterDevices = (prefs: EnergyPreferences): boolean =>
|
||||
(prefs.device_consumption_water?.length ?? 0) > 0;
|
||||
|
||||
export const hasDeviceConsumption = (prefs: EnergyPreferences): boolean =>
|
||||
prefs.device_consumption.length > 0;
|
||||
|
||||
export const hasPowerSources = (prefs: EnergyPreferences): boolean =>
|
||||
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;
|
||||
});
|
||||
|
||||
export const hasPowerDevices = (prefs: EnergyPreferences): boolean =>
|
||||
prefs.device_consumption.some((device) => device.stat_rate);
|
||||
|
||||
export const hasPowerWaterDevices = (prefs: EnergyPreferences): boolean =>
|
||||
(prefs.device_consumption_water ?? []).some((device) => device.stat_rate);
|
||||
|
||||
// --- Card catalog ----------------------------------------------------------
|
||||
|
||||
export interface EnergyCardCatalogEntry {
|
||||
/** Stable identifier and storage token: `<view>.<cardType>`. */
|
||||
key: string;
|
||||
view: EnergyViewPath;
|
||||
/** Localize key for the label shown in the customise dialog. */
|
||||
labelKey: LocalizeKeys;
|
||||
/** Whether this card is emitted for the given preferences. */
|
||||
isApplicable: (prefs: EnergyPreferences) => boolean;
|
||||
}
|
||||
|
||||
export const energyCardKey = (view: EnergyViewPath, cardType: string): string =>
|
||||
`${view}.${cardType}`;
|
||||
|
||||
const entry = (
|
||||
view: EnergyViewPath,
|
||||
cardType: string,
|
||||
labelKey: LocalizeKeys,
|
||||
isApplicable: (prefs: EnergyPreferences) => boolean
|
||||
): EnergyCardCatalogEntry => ({
|
||||
key: energyCardKey(view, cardType),
|
||||
view,
|
||||
labelKey,
|
||||
isApplicable,
|
||||
});
|
||||
|
||||
export const ENERGY_CARD_CATALOG: readonly EnergyCardCatalogEntry[] = [
|
||||
// --- Overview ---
|
||||
entry(
|
||||
"overview",
|
||||
"energy-distribution",
|
||||
"ui.panel.energy.cards.energy_distribution_title",
|
||||
(p) => hasGridSource(p) || hasBattery(p) || hasSolar(p)
|
||||
),
|
||||
entry(
|
||||
"overview",
|
||||
"energy-sources-table",
|
||||
"ui.panel.energy.cards.energy_sources_table_title",
|
||||
(p) => p.energy_sources.length > 0
|
||||
),
|
||||
entry(
|
||||
"overview",
|
||||
"power-sources-graph",
|
||||
"ui.panel.energy.cards.power_sources_graph_title",
|
||||
(p) => hasPowerSources(p)
|
||||
),
|
||||
entry(
|
||||
"overview",
|
||||
"energy-usage-graph",
|
||||
"ui.panel.energy.cards.energy_usage_graph_title",
|
||||
(p) => hasGridSource(p) || hasBattery(p)
|
||||
),
|
||||
entry(
|
||||
"overview",
|
||||
"energy-gas-graph",
|
||||
"ui.panel.energy.cards.energy_gas_graph_title",
|
||||
(p) => hasGasSource(p)
|
||||
),
|
||||
// One toggle gates the water row, which renders energy-water-graph (sources)
|
||||
// or, with only water devices, water-sankey.
|
||||
entry(
|
||||
"overview",
|
||||
"energy-water-graph",
|
||||
"ui.panel.energy.cards.energy_water_graph_title",
|
||||
(p) => hasWaterSource(p) || hasWaterDevices(p)
|
||||
),
|
||||
|
||||
// --- Electricity ---
|
||||
entry(
|
||||
"electricity",
|
||||
"energy-distribution",
|
||||
"ui.panel.energy.cards.energy_distribution_title",
|
||||
(p) => hasGridSource(p) || hasBattery(p) || hasSolar(p)
|
||||
),
|
||||
entry(
|
||||
"electricity",
|
||||
"energy-grid-balance",
|
||||
"ui.panel.energy.cards.energy_grid_balance_title",
|
||||
(p) => hasGridSource(p) && hasReturn(p)
|
||||
),
|
||||
entry(
|
||||
"electricity",
|
||||
"energy-grid-neutrality-gauge",
|
||||
"ui.panel.energy.cards.energy_grid_neutrality_gauge_title",
|
||||
(p) => hasReturn(p)
|
||||
),
|
||||
entry(
|
||||
"electricity",
|
||||
"energy-solar-consumed-gauge",
|
||||
"ui.panel.energy.cards.energy_solar_consumed_gauge_title",
|
||||
(p) => hasSolar(p) && hasReturn(p)
|
||||
),
|
||||
entry(
|
||||
"electricity",
|
||||
"energy-self-sufficiency-gauge",
|
||||
"ui.panel.energy.cards.energy_self_sufficiency_gauge_title",
|
||||
(p) => hasSolar(p) && hasGridSource(p)
|
||||
),
|
||||
entry(
|
||||
"electricity",
|
||||
"energy-carbon-consumed-gauge",
|
||||
"ui.panel.energy.cards.energy_carbon_consumed_gauge_title",
|
||||
(p) => hasGridSource(p)
|
||||
),
|
||||
entry(
|
||||
"electricity",
|
||||
"energy-usage-graph",
|
||||
"ui.panel.energy.cards.energy_usage_graph_title",
|
||||
(p) => hasGridSource(p) || hasBattery(p)
|
||||
),
|
||||
entry(
|
||||
"electricity",
|
||||
"energy-solar-graph",
|
||||
"ui.panel.energy.cards.energy_solar_graph_title",
|
||||
(p) => hasSolar(p)
|
||||
),
|
||||
entry(
|
||||
"electricity",
|
||||
"energy-sources-table",
|
||||
"ui.panel.energy.cards.energy_sources_table_title",
|
||||
(p) => hasGridSource(p) || hasSolar(p) || hasBattery(p)
|
||||
),
|
||||
entry(
|
||||
"electricity",
|
||||
"energy-devices-detail-graph",
|
||||
"ui.panel.energy.cards.energy_devices_detail_graph_title",
|
||||
(p) => hasDeviceConsumption(p)
|
||||
),
|
||||
entry(
|
||||
"electricity",
|
||||
"energy-devices-graph",
|
||||
"ui.panel.energy.cards.energy_devices_graph_title",
|
||||
(p) => hasDeviceConsumption(p)
|
||||
),
|
||||
entry(
|
||||
"electricity",
|
||||
"energy-sankey",
|
||||
"ui.panel.energy.cards.energy_sankey_title",
|
||||
(p) => hasDeviceConsumption(p)
|
||||
),
|
||||
|
||||
// --- Gas ---
|
||||
entry(
|
||||
"gas",
|
||||
"energy-gas-graph",
|
||||
"ui.panel.energy.cards.energy_gas_graph_title",
|
||||
(p) => hasGasSource(p)
|
||||
),
|
||||
entry(
|
||||
"gas",
|
||||
"energy-sources-table",
|
||||
"ui.panel.energy.cards.energy_sources_table_title",
|
||||
(p) => hasGasSource(p)
|
||||
),
|
||||
|
||||
// --- Water ---
|
||||
entry(
|
||||
"water",
|
||||
"energy-water-graph",
|
||||
"ui.panel.energy.cards.energy_water_graph_title",
|
||||
(p) => hasWaterSource(p)
|
||||
),
|
||||
entry(
|
||||
"water",
|
||||
"energy-sources-table",
|
||||
"ui.panel.energy.cards.energy_sources_table_title",
|
||||
(p) => hasWaterSource(p)
|
||||
),
|
||||
entry(
|
||||
"water",
|
||||
"water-sankey",
|
||||
"ui.panel.energy.cards.water_sankey_title",
|
||||
(p) => hasWaterDevices(p)
|
||||
),
|
||||
|
||||
// --- Now (power) ---
|
||||
entry(
|
||||
"now",
|
||||
"power-sources-graph",
|
||||
"ui.panel.energy.cards.power_sources_graph_title",
|
||||
(p) => hasPowerSources(p)
|
||||
),
|
||||
entry(
|
||||
"now",
|
||||
"power-sankey",
|
||||
"ui.panel.energy.cards.power_sankey_title",
|
||||
(p) => hasPowerDevices(p)
|
||||
),
|
||||
entry(
|
||||
"now",
|
||||
"water-flow-sankey",
|
||||
"ui.panel.energy.cards.water_flow_sankey_title",
|
||||
(p) => hasPowerWaterDevices(p)
|
||||
),
|
||||
];
|
||||
|
||||
// --- Lookup helpers --------------------------------------------------------
|
||||
|
||||
export const isEnergyCardHidden = (
|
||||
view: EnergyViewPath,
|
||||
cardType: string,
|
||||
hidden: string[] | undefined
|
||||
): boolean => !!hidden?.includes(energyCardKey(view, cardType));
|
||||
|
||||
/** Keys of all catalog cards that apply to the given preferences for a view. */
|
||||
export const applicableEnergyCardKeys = (
|
||||
view: EnergyViewPath,
|
||||
prefs: EnergyPreferences
|
||||
): string[] =>
|
||||
ENERGY_CARD_CATALOG.filter(
|
||||
(c) => c.view === view && c.isApplicable(prefs)
|
||||
).map((c) => c.key);
|
||||
|
||||
/** True when a view has applicable cards but every one of them is hidden. */
|
||||
export const isEnergyViewEmpty = (
|
||||
view: EnergyViewPath,
|
||||
prefs: EnergyPreferences,
|
||||
hidden: string[] | undefined
|
||||
): boolean => {
|
||||
const applicable = applicableEnergyCardKeys(view, prefs);
|
||||
return (
|
||||
applicable.length > 0 && applicable.every((key) => hidden?.includes(key))
|
||||
);
|
||||
};
|
||||
@@ -7,13 +7,15 @@ import {
|
||||
import type { EnergyPreferences } from "../../../data/energy";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceConfig } from "../../../data/lovelace/config/types";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { LovelaceStrategyViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { LocalizeKeys } from "../../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import {
|
||||
DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
DEFAULT_POWER_COLLECTION_KEY,
|
||||
} from "../constants";
|
||||
import type { EnergyViewPath } from "./energy-cards";
|
||||
import { isEnergyViewEmpty } from "./energy-cards";
|
||||
|
||||
const OVERVIEW_VIEW = {
|
||||
path: "overview",
|
||||
@@ -21,7 +23,7 @@ const OVERVIEW_VIEW = {
|
||||
type: "energy-overview",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
},
|
||||
} as LovelaceViewConfig;
|
||||
} as LovelaceStrategyViewConfig;
|
||||
|
||||
const ENERGY_VIEW = {
|
||||
path: "electricity",
|
||||
@@ -29,7 +31,7 @@ const ENERGY_VIEW = {
|
||||
type: "energy",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
},
|
||||
} as LovelaceViewConfig;
|
||||
} as LovelaceStrategyViewConfig;
|
||||
|
||||
const WATER_VIEW = {
|
||||
path: "water",
|
||||
@@ -37,7 +39,7 @@ const WATER_VIEW = {
|
||||
type: "water",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
},
|
||||
} as LovelaceViewConfig;
|
||||
} as LovelaceStrategyViewConfig;
|
||||
|
||||
const GAS_VIEW = {
|
||||
path: "gas",
|
||||
@@ -45,7 +47,7 @@ const GAS_VIEW = {
|
||||
type: "gas",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
},
|
||||
} as LovelaceViewConfig;
|
||||
} as LovelaceStrategyViewConfig;
|
||||
|
||||
const POWER_VIEW = {
|
||||
path: "now",
|
||||
@@ -53,7 +55,7 @@ const POWER_VIEW = {
|
||||
type: "power",
|
||||
collection_key: DEFAULT_POWER_COLLECTION_KEY,
|
||||
},
|
||||
} as LovelaceViewConfig;
|
||||
} as LovelaceStrategyViewConfig;
|
||||
|
||||
const WIZARD_VIEW = {
|
||||
type: "panel",
|
||||
@@ -64,6 +66,7 @@ const WIZARD_VIEW = {
|
||||
export interface EnergyDashboardStrategyConfig extends LovelaceStrategyConfig {
|
||||
type: "energy";
|
||||
default_collection?: string;
|
||||
hidden_cards?: string[];
|
||||
}
|
||||
|
||||
@customElement("energy-dashboard-strategy")
|
||||
@@ -112,28 +115,42 @@ export class EnergyDashboardStrategy extends ReactiveElement {
|
||||
|
||||
const hasDeviceConsumption = prefs.device_consumption.length > 0;
|
||||
|
||||
const views: LovelaceViewConfig[] = [];
|
||||
const hidden = _config.hidden_cards;
|
||||
|
||||
const candidateViews: LovelaceStrategyViewConfig[] = [];
|
||||
if (hasEnergy || hasDeviceConsumption) {
|
||||
views.push(ENERGY_VIEW);
|
||||
candidateViews.push(ENERGY_VIEW);
|
||||
}
|
||||
if (hasGas) {
|
||||
views.push(GAS_VIEW);
|
||||
candidateViews.push(GAS_VIEW);
|
||||
}
|
||||
if (hasWater) {
|
||||
views.push(WATER_VIEW);
|
||||
candidateViews.push(WATER_VIEW);
|
||||
}
|
||||
if (hasPower) {
|
||||
views.push(POWER_VIEW);
|
||||
candidateViews.push(POWER_VIEW);
|
||||
}
|
||||
if (
|
||||
hasPowerSource ||
|
||||
[hasEnergy, hasGas, hasWater].filter(Boolean).length > 1
|
||||
) {
|
||||
views.unshift(OVERVIEW_VIEW);
|
||||
candidateViews.unshift(OVERVIEW_VIEW);
|
||||
}
|
||||
|
||||
// Drop a view (tab) when every card it would render has been hidden, so we
|
||||
// don't show an empty tab. Keep at least one view so the dashboard never
|
||||
// renders blank and the customize entry stays reachable.
|
||||
let views = candidateViews.filter(
|
||||
(view) => !isEnergyViewEmpty(view.path as EnergyViewPath, prefs, hidden)
|
||||
);
|
||||
if (views.length === 0) {
|
||||
views = candidateViews;
|
||||
}
|
||||
|
||||
return {
|
||||
views: views.map((view) => ({
|
||||
...view,
|
||||
strategy: { ...view.strategy, hidden_cards: hidden },
|
||||
title:
|
||||
view.title ||
|
||||
hass.localize(`ui.panel.energy.title.${view.path}` as LocalizeKeys),
|
||||
|
||||
@@ -4,17 +4,19 @@ import type { GridSourceTypeEnergyPreference } from "../../../data/energy";
|
||||
import { getEnergyDataCollection } from "../../../data/energy";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
|
||||
import type { EnergyViewStrategyConfig } from "./energy-cards";
|
||||
import { isEnergyCardHidden } from "./energy-cards";
|
||||
|
||||
@customElement("energy-overview-view-strategy")
|
||||
export class EnergyOverviewViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
_config: LovelaceStrategyConfig,
|
||||
_config: EnergyViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const collectionKey =
|
||||
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
|
||||
const hidden = _config.hidden_cards;
|
||||
|
||||
const view: LovelaceViewConfig = {
|
||||
type: "sections",
|
||||
@@ -73,7 +75,10 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasGrid || hasBattery || hasSolar) {
|
||||
if (
|
||||
(hasGrid || hasBattery || hasSolar) &&
|
||||
!isEnergyCardHidden("overview", "energy-distribution", hidden)
|
||||
) {
|
||||
view.sections!.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
@@ -88,7 +93,10 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (prefs.energy_sources.length) {
|
||||
if (
|
||||
prefs.energy_sources.length &&
|
||||
!isEnergyCardHidden("overview", "energy-sources-table", hidden)
|
||||
) {
|
||||
view.sections!.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
@@ -104,7 +112,10 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (hasPowerSources) {
|
||||
if (
|
||||
hasPowerSources &&
|
||||
!isEnergyCardHidden("overview", "power-sources-graph", hidden)
|
||||
) {
|
||||
view.sections!.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
@@ -120,7 +131,10 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (hasGrid || hasBattery) {
|
||||
if (
|
||||
(hasGrid || hasBattery) &&
|
||||
!isEnergyCardHidden("overview", "energy-usage-graph", hidden)
|
||||
) {
|
||||
view.sections!.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
@@ -135,7 +149,7 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (hasGas) {
|
||||
if (hasGas && !isEnergyCardHidden("overview", "energy-gas-graph", hidden)) {
|
||||
view.sections!.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
@@ -150,7 +164,10 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (hasWaterSources || hasWaterDevices) {
|
||||
if (
|
||||
(hasWaterSources || hasWaterDevices) &&
|
||||
!isEnergyCardHidden("overview", "energy-water-graph", hidden)
|
||||
) {
|
||||
view.sections!.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
|
||||
@@ -3,10 +3,11 @@ import { customElement } from "lit/decorators";
|
||||
import type { GridSourceTypeEnergyPreference } from "../../../data/energy";
|
||||
import { getEnergyDataCollection } from "../../../data/energy";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
|
||||
import type { EnergyViewStrategyConfig } from "./energy-cards";
|
||||
import { isEnergyCardHidden } from "./energy-cards";
|
||||
import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
|
||||
import {
|
||||
LARGE_SCREEN_CONDITION,
|
||||
@@ -16,11 +17,12 @@ import {
|
||||
@customElement("energy-view-strategy")
|
||||
export class EnergyViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
_config: LovelaceStrategyConfig,
|
||||
_config: EnergyViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const collectionKey =
|
||||
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
|
||||
const hidden = _config.hidden_cards;
|
||||
|
||||
const view: LovelaceViewConfig = {
|
||||
type: "sections",
|
||||
@@ -75,7 +77,10 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
const gaugeCards: LovelaceCardConfig[] = [];
|
||||
const sidebarSection = view.sidebar!.sections![0];
|
||||
|
||||
if (hasGrid || hasBattery || hasSolar) {
|
||||
if (
|
||||
(hasGrid || hasBattery || hasSolar) &&
|
||||
!isEnergyCardHidden("electricity", "energy-distribution", hidden)
|
||||
) {
|
||||
const distributionCard = {
|
||||
title: hass.localize("ui.panel.energy.cards.energy_distribution_title"),
|
||||
type: "energy-distribution",
|
||||
@@ -91,7 +96,11 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
}
|
||||
|
||||
// Only include if we have both grid import and export configured
|
||||
if (hasGrid && hasReturn) {
|
||||
if (
|
||||
hasGrid &&
|
||||
hasReturn &&
|
||||
!isEnergyCardHidden("electricity", "energy-grid-balance", hidden)
|
||||
) {
|
||||
const gridResultCard = {
|
||||
type: "energy-grid-balance",
|
||||
collection_key: collectionKey,
|
||||
@@ -106,7 +115,10 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
}
|
||||
|
||||
// Only include if we have a grid source & return.
|
||||
if (hasReturn) {
|
||||
if (
|
||||
hasReturn &&
|
||||
!isEnergyCardHidden("electricity", "energy-grid-neutrality-gauge", hidden)
|
||||
) {
|
||||
const card = {
|
||||
type: "energy-grid-neutrality-gauge",
|
||||
collection_key: collectionKey,
|
||||
@@ -116,14 +128,28 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
|
||||
// Only include if we have a solar source.
|
||||
if (hasSolar) {
|
||||
if (hasReturn) {
|
||||
if (
|
||||
hasReturn &&
|
||||
!isEnergyCardHidden(
|
||||
"electricity",
|
||||
"energy-solar-consumed-gauge",
|
||||
hidden
|
||||
)
|
||||
) {
|
||||
const card = {
|
||||
type: "energy-solar-consumed-gauge",
|
||||
collection_key: collectionKey,
|
||||
};
|
||||
gaugeCards.push(card);
|
||||
}
|
||||
if (hasGrid) {
|
||||
if (
|
||||
hasGrid &&
|
||||
!isEnergyCardHidden(
|
||||
"electricity",
|
||||
"energy-self-sufficiency-gauge",
|
||||
hidden
|
||||
)
|
||||
) {
|
||||
const card = {
|
||||
type: "energy-self-sufficiency-gauge",
|
||||
collection_key: collectionKey,
|
||||
@@ -133,7 +159,10 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
}
|
||||
|
||||
// Only include if we have a grid
|
||||
if (hasGrid) {
|
||||
if (
|
||||
hasGrid &&
|
||||
!isEnergyCardHidden("electricity", "energy-carbon-consumed-gauge", hidden)
|
||||
) {
|
||||
const card = {
|
||||
type: "energy-carbon-consumed-gauge",
|
||||
collection_key: collectionKey,
|
||||
@@ -168,7 +197,10 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
});
|
||||
|
||||
// Only include if we have a grid or battery.
|
||||
if (hasGrid || hasBattery) {
|
||||
if (
|
||||
(hasGrid || hasBattery) &&
|
||||
!isEnergyCardHidden("electricity", "energy-usage-graph", hidden)
|
||||
) {
|
||||
mainCards.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_usage_graph_title"),
|
||||
type: "energy-usage-graph",
|
||||
@@ -178,7 +210,10 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
}
|
||||
|
||||
// Only include if we have a solar source.
|
||||
if (hasSolar) {
|
||||
if (
|
||||
hasSolar &&
|
||||
!isEnergyCardHidden("electricity", "energy-solar-graph", hidden)
|
||||
) {
|
||||
mainCards.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_solar_graph_title"),
|
||||
type: "energy-solar-graph",
|
||||
@@ -187,7 +222,10 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (hasGrid || hasSolar || hasBattery) {
|
||||
if (
|
||||
(hasGrid || hasSolar || hasBattery) &&
|
||||
!isEnergyCardHidden("electricity", "energy-sources-table", hidden)
|
||||
) {
|
||||
mainCards.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_sources_table_title"
|
||||
@@ -201,35 +239,47 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
|
||||
// Only include if we have at least 1 device in the config.
|
||||
if (prefs.device_consumption.length) {
|
||||
const showFloorsAndAreas = shouldShowFloorsAndAreas(
|
||||
prefs.device_consumption,
|
||||
hass,
|
||||
(d) => d.stat_consumption
|
||||
);
|
||||
mainCards.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_devices_detail_graph_title"
|
||||
),
|
||||
type: "energy-devices-detail-graph",
|
||||
collection_key: collectionKey,
|
||||
grid_options: { columns: 36 },
|
||||
});
|
||||
mainCards.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_devices_graph_title"
|
||||
),
|
||||
type: "energy-devices-graph",
|
||||
collection_key: collectionKey,
|
||||
grid_options: { columns: 36 },
|
||||
});
|
||||
mainCards.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_sankey_title"),
|
||||
type: "energy-sankey",
|
||||
collection_key: collectionKey,
|
||||
group_by_floor: showFloorsAndAreas,
|
||||
group_by_area: showFloorsAndAreas,
|
||||
grid_options: { columns: 36 },
|
||||
});
|
||||
if (
|
||||
!isEnergyCardHidden(
|
||||
"electricity",
|
||||
"energy-devices-detail-graph",
|
||||
hidden
|
||||
)
|
||||
) {
|
||||
mainCards.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_devices_detail_graph_title"
|
||||
),
|
||||
type: "energy-devices-detail-graph",
|
||||
collection_key: collectionKey,
|
||||
grid_options: { columns: 36 },
|
||||
});
|
||||
}
|
||||
if (!isEnergyCardHidden("electricity", "energy-devices-graph", hidden)) {
|
||||
mainCards.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_devices_graph_title"
|
||||
),
|
||||
type: "energy-devices-graph",
|
||||
collection_key: collectionKey,
|
||||
grid_options: { columns: 36 },
|
||||
});
|
||||
}
|
||||
if (!isEnergyCardHidden("electricity", "energy-sankey", hidden)) {
|
||||
const showFloorsAndAreas = shouldShowFloorsAndAreas(
|
||||
prefs.device_consumption,
|
||||
hass,
|
||||
(d) => d.stat_consumption
|
||||
);
|
||||
mainCards.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_sankey_title"),
|
||||
type: "energy-sankey",
|
||||
collection_key: collectionKey,
|
||||
group_by_floor: showFloorsAndAreas,
|
||||
group_by_area: showFloorsAndAreas,
|
||||
grid_options: { columns: 36 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
view.sections!.push({
|
||||
|
||||
@@ -3,18 +3,20 @@ import { customElement } from "lit/decorators";
|
||||
import { getEnergyDataCollection } from "../../../data/energy";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
|
||||
import type { EnergyViewStrategyConfig } from "./energy-cards";
|
||||
import { isEnergyCardHidden } from "./energy-cards";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
|
||||
@customElement("gas-view-strategy")
|
||||
export class GasViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
_config: LovelaceStrategyConfig,
|
||||
_config: EnergyViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const collectionKey =
|
||||
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
|
||||
const hidden = _config.hidden_cards;
|
||||
|
||||
const view: LovelaceViewConfig = {
|
||||
type: "sections",
|
||||
@@ -57,24 +59,30 @@ export class GasViewStrategy extends ReactiveElement {
|
||||
},
|
||||
});
|
||||
|
||||
section.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_gas_graph_title"),
|
||||
type: "energy-gas-graph",
|
||||
collection_key: collectionKey,
|
||||
grid_options: {
|
||||
columns: 24,
|
||||
},
|
||||
});
|
||||
if (!isEnergyCardHidden("gas", "energy-gas-graph", hidden)) {
|
||||
section.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_gas_graph_title"),
|
||||
type: "energy-gas-graph",
|
||||
collection_key: collectionKey,
|
||||
grid_options: {
|
||||
columns: 24,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
section.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_sources_table_title"),
|
||||
type: "energy-sources-table",
|
||||
collection_key: collectionKey,
|
||||
types: ["gas"],
|
||||
grid_options: {
|
||||
columns: 12,
|
||||
},
|
||||
});
|
||||
if (!isEnergyCardHidden("gas", "energy-sources-table", hidden)) {
|
||||
section.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_sources_table_title"
|
||||
),
|
||||
type: "energy-sources-table",
|
||||
collection_key: collectionKey,
|
||||
types: ["gas"],
|
||||
grid_options: {
|
||||
columns: 12,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { getEnergyDataCollection } from "../../../data/energy";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
|
||||
import type { EnergyViewStrategyConfig } from "./energy-cards";
|
||||
import { isEnergyCardHidden } from "./energy-cards";
|
||||
import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
@@ -12,11 +13,12 @@ import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
@customElement("power-view-strategy")
|
||||
export class PowerViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
_config: LovelaceStrategyConfig,
|
||||
_config: EnergyViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const collectionKey =
|
||||
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
|
||||
const hidden = _config.hidden_cards;
|
||||
|
||||
const energyCollection = getEnergyDataCollection(hass, {
|
||||
key: collectionKey,
|
||||
@@ -76,14 +78,18 @@ export class PowerViewStrategy extends ReactiveElement {
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
|
||||
chartsSection.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.power_sources_graph_title"),
|
||||
type: "power-sources-graph",
|
||||
collection_key: collectionKey,
|
||||
grid_options: {
|
||||
columns: 36,
|
||||
},
|
||||
});
|
||||
if (!isEnergyCardHidden("now", "power-sources-graph", hidden)) {
|
||||
chartsSection.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.power_sources_graph_title"
|
||||
),
|
||||
type: "power-sources-graph",
|
||||
collection_key: collectionKey,
|
||||
grid_options: {
|
||||
columns: 36,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (hasGasSources) {
|
||||
@@ -109,7 +115,7 @@ export class PowerViewStrategy extends ReactiveElement {
|
||||
}
|
||||
});
|
||||
|
||||
if (hasPowerDevices) {
|
||||
if (hasPowerDevices && !isEnergyCardHidden("now", "power-sankey", hidden)) {
|
||||
const showFloorsAndAreas = shouldShowFloorsAndAreas(
|
||||
prefs.device_consumption,
|
||||
hass,
|
||||
@@ -127,7 +133,10 @@ export class PowerViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (hasWaterDevices) {
|
||||
if (
|
||||
hasWaterDevices &&
|
||||
!isEnergyCardHidden("now", "water-flow-sankey", hidden)
|
||||
) {
|
||||
const showFloorsAndAreas = shouldShowFloorsAndAreas(
|
||||
prefs.device_consumption_water,
|
||||
hass,
|
||||
|
||||
@@ -2,20 +2,22 @@ import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { getEnergyDataCollection } from "../../../data/energy";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
|
||||
import type { EnergyViewStrategyConfig } from "./energy-cards";
|
||||
import { isEnergyCardHidden } from "./energy-cards";
|
||||
import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
|
||||
|
||||
@customElement("water-view-strategy")
|
||||
export class WaterViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
_config: LovelaceStrategyConfig,
|
||||
_config: EnergyViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const collectionKey =
|
||||
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
|
||||
const hidden = _config.hidden_cards;
|
||||
|
||||
const view: LovelaceViewConfig = {
|
||||
type: "sections",
|
||||
@@ -60,29 +62,38 @@ export class WaterViewStrategy extends ReactiveElement {
|
||||
});
|
||||
|
||||
if (hasWaterSources) {
|
||||
section.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_water_graph_title"),
|
||||
type: "energy-water-graph",
|
||||
collection_key: collectionKey,
|
||||
grid_options: {
|
||||
columns: 24,
|
||||
},
|
||||
});
|
||||
section.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_sources_table_title"
|
||||
),
|
||||
type: "energy-sources-table",
|
||||
collection_key: collectionKey,
|
||||
types: ["water"],
|
||||
grid_options: {
|
||||
columns: 12,
|
||||
},
|
||||
});
|
||||
if (!isEnergyCardHidden("water", "energy-water-graph", hidden)) {
|
||||
section.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_water_graph_title"
|
||||
),
|
||||
type: "energy-water-graph",
|
||||
collection_key: collectionKey,
|
||||
grid_options: {
|
||||
columns: 24,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!isEnergyCardHidden("water", "energy-sources-table", hidden)) {
|
||||
section.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_sources_table_title"
|
||||
),
|
||||
type: "energy-sources-table",
|
||||
collection_key: collectionKey,
|
||||
types: ["water"],
|
||||
grid_options: {
|
||||
columns: 12,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Only include if we have at least 1 water device in the config.
|
||||
if (hasWaterDevices) {
|
||||
if (
|
||||
hasWaterDevices &&
|
||||
!isEnergyCardHidden("water", "water-sankey", hidden)
|
||||
) {
|
||||
const showFloorsAndAreas = shouldShowFloorsAndAreas(
|
||||
prefs.device_consumption_water,
|
||||
hass,
|
||||
|
||||
@@ -438,7 +438,9 @@ class HuiEnergySankeyCard
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)} kWh`;
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)}
|
||||
kWh</div>`;
|
||||
|
||||
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
|
||||
const { node } = ev.detail;
|
||||
|
||||
@@ -580,7 +580,9 @@ class HuiPowerSankeyCard
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
formatPowerShort(this.hass, value);
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatPowerShort(this.hass, value)}
|
||||
</div>`;
|
||||
|
||||
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
|
||||
const { node } = ev.detail;
|
||||
|
||||
@@ -511,11 +511,9 @@ class HuiWaterFlowSankeyCard
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
formatFlowRateShort(
|
||||
this.hass.locale,
|
||||
this.hass.config.unit_system.length,
|
||||
value
|
||||
);
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatFlowRateShort(this.hass.locale, this.hass.config.unit_system.length, value)}
|
||||
</div>`;
|
||||
|
||||
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
|
||||
const { node } = ev.detail;
|
||||
|
||||
@@ -30,7 +30,6 @@ import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
@@ -231,28 +230,11 @@ export class HaCardConditionEditor extends LitElement {
|
||||
return html`
|
||||
<div class="container">
|
||||
<ha-expansion-panel left-chevron>
|
||||
<div
|
||||
id="condition-icon"
|
||||
class="icon-badge-wrapper"
|
||||
<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${ICON_CONDITION[condition.condition]}
|
||||
></ha-svg-icon>
|
||||
${hideLiveTest
|
||||
? nothing
|
||||
: html`<ha-automation-row-live-test
|
||||
.state=${this._liveTestResult.state}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
|
||||
)}
|
||||
></ha-automation-row-live-test>`}
|
||||
</div>
|
||||
${!hideLiveTest && this._liveTestResult.message
|
||||
? html`<ha-tooltip for="condition-icon" slot="leading-icon"
|
||||
>${this._liveTestResult.message}</ha-tooltip
|
||||
>`
|
||||
: nothing}
|
||||
class="condition-icon"
|
||||
.path=${ICON_CONDITION[condition.condition]}
|
||||
></ha-svg-icon>
|
||||
<h3 slot="header">
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.condition.${condition.condition}.label`
|
||||
@@ -273,6 +255,18 @@ export class HaCardConditionEditor extends LitElement {
|
||||
"ui.panel.lovelace.editor.condition-editor.testing_error"
|
||||
)}
|
||||
</ha-automation-row-event-chip>
|
||||
${hideLiveTest
|
||||
? nothing
|
||||
: html`
|
||||
<ha-automation-row-live-test
|
||||
slot="icons"
|
||||
.state=${this._liveTestResult.state}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
|
||||
)}
|
||||
.message=${this._liveTestResult.message}
|
||||
></ha-automation-row-live-test>
|
||||
`}
|
||||
<ha-dropdown
|
||||
slot="icons"
|
||||
@wa-select=${this._handleAction}
|
||||
@@ -485,15 +479,17 @@ export class HaCardConditionEditor extends LitElement {
|
||||
--expansion-panel-summary-padding: 0 0 0 8px;
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.icon-badge-wrapper {
|
||||
.condition-icon {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 870px) {
|
||||
.icon-badge-wrapper {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
.condition-icon {
|
||||
display: inline-block;
|
||||
color: var(--secondary-text-color);
|
||||
opacity: 0.9;
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
|
||||
@@ -4138,6 +4138,18 @@
|
||||
"gas": "Gas",
|
||||
"water": "Water"
|
||||
},
|
||||
"customise": {
|
||||
"toolbar_action": "Customize cards",
|
||||
"title": "Customize energy",
|
||||
"subtitle": "Toggle to show or hide cards",
|
||||
"saved": "Energy dashboard updated",
|
||||
"save_failed": "Failed to save energy customization",
|
||||
"unavailable": "This card isn't shown because the energy source or device it needs isn't configured.",
|
||||
"groups": {
|
||||
"overview": "Overview",
|
||||
"now": "Now"
|
||||
}
|
||||
},
|
||||
"delete_source": "Are you sure you want to remove this source?",
|
||||
"delete_integration": "Are you sure you want to remove this integration? It will remove the entities it provides",
|
||||
"grid": {
|
||||
@@ -11188,7 +11200,12 @@
|
||||
"energy_top_consumers_title": "Top consumers",
|
||||
"power_sankey_title": "Current power flow",
|
||||
"water_flow_sankey_title": "Current water flow",
|
||||
"power_sources_graph_title": "Power sources"
|
||||
"power_sources_graph_title": "Power sources",
|
||||
"energy_grid_balance_title": "Grid energy balance",
|
||||
"energy_grid_neutrality_gauge_title": "Grid neutrality gauge",
|
||||
"energy_solar_consumed_gauge_title": "Solar consumed gauge",
|
||||
"energy_self_sufficiency_gauge_title": "Self-sufficiency gauge",
|
||||
"energy_carbon_consumed_gauge_title": "Carbon consumed gauge"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type {
|
||||
EnergyPreferences,
|
||||
EnergySource,
|
||||
} from "../../../../src/data/energy";
|
||||
import {
|
||||
applicableEnergyCardKeys,
|
||||
ENERGY_CARD_CATALOG,
|
||||
energyCardKey,
|
||||
isEnergyCardHidden,
|
||||
isEnergyViewEmpty,
|
||||
} from "../../../../src/panels/energy/strategies/energy-cards";
|
||||
|
||||
const source = (s: Partial<EnergySource> & { type: string }): EnergySource =>
|
||||
s as unknown as EnergySource;
|
||||
|
||||
const makePrefs = (
|
||||
prefs: Partial<EnergyPreferences> = {}
|
||||
): EnergyPreferences => ({
|
||||
energy_sources: [],
|
||||
device_consumption: [],
|
||||
device_consumption_water: [],
|
||||
...prefs,
|
||||
});
|
||||
|
||||
const GRID_RETURN = source({
|
||||
type: "grid",
|
||||
stat_energy_from: "sensor.grid_in",
|
||||
stat_energy_to: "sensor.grid_out",
|
||||
});
|
||||
const SOLAR = source({ type: "solar", stat_energy_from: "sensor.solar" });
|
||||
const GAS = source({ type: "gas", stat_energy_from: "sensor.gas" });
|
||||
const WATER = source({ type: "water", stat_energy_from: "sensor.water" });
|
||||
|
||||
describe("energyCardKey", () => {
|
||||
it("joins the view path and card type", () => {
|
||||
expect(energyCardKey("electricity", "energy-solar-graph")).toBe(
|
||||
"electricity.energy-solar-graph"
|
||||
);
|
||||
expect(energyCardKey("now", "power-sankey")).toBe("now.power-sankey");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isEnergyCardHidden", () => {
|
||||
it("returns true only when the composite key is in the hidden list", () => {
|
||||
const hidden = ["electricity.energy-solar-graph"];
|
||||
expect(
|
||||
isEnergyCardHidden("electricity", "energy-solar-graph", hidden)
|
||||
).toBe(true);
|
||||
// Same card type in a different view is independent.
|
||||
expect(isEnergyCardHidden("overview", "energy-solar-graph", hidden)).toBe(
|
||||
false
|
||||
);
|
||||
expect(
|
||||
isEnergyCardHidden("electricity", "energy-usage-graph", hidden)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("treats undefined/empty hidden lists as nothing hidden", () => {
|
||||
expect(
|
||||
isEnergyCardHidden("electricity", "energy-solar-graph", undefined)
|
||||
).toBe(false);
|
||||
expect(isEnergyCardHidden("electricity", "energy-solar-graph", [])).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("catalog applicability", () => {
|
||||
it("only lists cards relevant to the configured sources", () => {
|
||||
const gasOnly = makePrefs({ energy_sources: [GAS] });
|
||||
expect(applicableEnergyCardKeys("gas", gasOnly)).toEqual([
|
||||
"gas.energy-gas-graph",
|
||||
"gas.energy-sources-table",
|
||||
]);
|
||||
// No electricity sources -> no electricity cards apply.
|
||||
expect(applicableEnergyCardKeys("electricity", gasOnly)).toEqual([]);
|
||||
});
|
||||
|
||||
it("gates the solar graph and gauges on their sources", () => {
|
||||
const solarGraph = ENERGY_CARD_CATALOG.find(
|
||||
(c) => c.key === "electricity.energy-solar-graph"
|
||||
)!;
|
||||
expect(
|
||||
solarGraph.isApplicable(makePrefs({ energy_sources: [SOLAR] }))
|
||||
).toBe(true);
|
||||
expect(
|
||||
solarGraph.isApplicable(makePrefs({ energy_sources: [GRID_RETURN] }))
|
||||
).toBe(false);
|
||||
|
||||
const neutralityGauge = ENERGY_CARD_CATALOG.find(
|
||||
(c) => c.key === "electricity.energy-grid-neutrality-gauge"
|
||||
)!;
|
||||
// Needs grid export (return).
|
||||
expect(
|
||||
neutralityGauge.isApplicable(makePrefs({ energy_sources: [GRID_RETURN] }))
|
||||
).toBe(true);
|
||||
expect(
|
||||
neutralityGauge.isApplicable(makePrefs({ energy_sources: [SOLAR] }))
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isEnergyViewEmpty", () => {
|
||||
const prefs = makePrefs({ energy_sources: [WATER] });
|
||||
|
||||
it("is false when no cards in the view are hidden", () => {
|
||||
expect(isEnergyViewEmpty("water", prefs, undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("is false when only some applicable cards are hidden", () => {
|
||||
expect(
|
||||
isEnergyViewEmpty("water", prefs, ["water.energy-water-graph"])
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("is true when every applicable card is hidden", () => {
|
||||
expect(
|
||||
isEnergyViewEmpty("water", prefs, [
|
||||
"water.energy-water-graph",
|
||||
"water.energy-sources-table",
|
||||
])
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("is false when the view has no applicable cards at all", () => {
|
||||
// Water source configured, but the gas view has nothing applicable.
|
||||
expect(isEnergyViewEmpty("gas", prefs, [])).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -8557,7 +8557,7 @@ __metadata:
|
||||
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||
leaflet.markercluster: "npm:1.5.3"
|
||||
license-checker-rseidelsohn: "npm:5.0.1"
|
||||
lint-staged: "npm:17.0.6"
|
||||
lint-staged: "npm:17.0.5"
|
||||
lit: "npm:3.3.3"
|
||||
lit-analyzer: "npm:2.0.3"
|
||||
lit-html: "npm:3.3.3"
|
||||
@@ -9936,21 +9936,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lint-staged@npm:17.0.6":
|
||||
version: 17.0.6
|
||||
resolution: "lint-staged@npm:17.0.6"
|
||||
"lint-staged@npm:17.0.5":
|
||||
version: 17.0.5
|
||||
resolution: "lint-staged@npm:17.0.5"
|
||||
dependencies:
|
||||
listr2: "npm:^10.2.1"
|
||||
picomatch: "npm:^4.0.4"
|
||||
string-argv: "npm:^0.3.2"
|
||||
tinyexec: "npm:1.2.2"
|
||||
yaml: "npm:^2.9.0"
|
||||
tinyexec: "npm:^1.1.2"
|
||||
yaml: "npm:^2.8.4"
|
||||
dependenciesMeta:
|
||||
yaml:
|
||||
optional: true
|
||||
bin:
|
||||
lint-staged: bin/lint-staged.js
|
||||
checksum: 10/371918cfb293ed0ca5d16fc2a1de304b5a95d21b87dc1ea7f3751567c8f8a07971a40349fac8edb5fce3c6ea6713f70922ea90184b142fd432a5bb4db6c316b0
|
||||
checksum: 10/a0bea43689d68ec0bf6a56943884dbdb96b6b49e2677bf80654d802678b2edf9fc65338ca8ef3fc310f245933ea2a809db1ac94431dc445c57a4d49620d9d4da
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -13172,10 +13172,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinyexec@npm:1.2.2, tinyexec@npm:^1.0.2":
|
||||
version: 1.2.2
|
||||
resolution: "tinyexec@npm:1.2.2"
|
||||
checksum: 10/e6f3cabafc33a46063868b4e9c0ab76722e21cb46f0177f7bef5a9656e09ea6fa37115d3e47f776aff11aab9ab696b0c840c8e0099fab574b1e37767c4371aec
|
||||
"tinyexec@npm:^1.0.2, tinyexec@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "tinyexec@npm:1.1.2"
|
||||
checksum: 10/2bbe37f9001c6f5723ab39eb8dc1e88f77e830d7cf2e8f34bb75019eb505fcfe3b061b4799c502ff31fa63aa1a9adc649add5ff1e17b7fbd8c16e1afb75d0b9e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -14755,7 +14755,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yaml@npm:^2.9.0":
|
||||
"yaml@npm:^2.8.4":
|
||||
version: 2.9.0
|
||||
resolution: "yaml@npm:2.9.0"
|
||||
bin:
|
||||
|
||||
Reference in New Issue
Block a user