Compare commits

...

6 Commits

Author SHA1 Message Date
Bram Kragten
df7a36e743 Update text 2025-11-14 16:24:46 +01:00
Bram Kragten
5786fe4b8d update link 2025-11-14 16:18:33 +01:00
Bram Kragten
6fa274e4bf Add device database toggle to analytics 2025-11-14 16:10:46 +01:00
Aidan Timson
1bd1e015ff Migrate dialog-lovelace-resource-detail to ha-wa-dialog (#27939) 2025-11-14 08:32:41 +02:00
Aidan Timson
7588490419 Migrate dialog-config-entry-system-options to ha-wa-dialog (#27938) 2025-11-14 08:27:17 +02:00
Petar Petrov
2e80a3ddab Add configurable chart modes in energy devices graph card (#27937) 2025-11-14 08:16:36 +02:00
7 changed files with 215 additions and 116 deletions

View File

@@ -5,6 +5,7 @@ export interface AnalyticsPreferences {
diagnostics?: boolean; diagnostics?: boolean;
usage?: boolean; usage?: boolean;
statistics?: boolean; statistics?: boolean;
snapshots?: boolean;
} }
export interface Analytics { export interface Analytics {

View File

@@ -2,7 +2,8 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { createCloseHeading } from "../../components/ha-dialog"; import "../../components/ha-wa-dialog";
import "../../components/ha-dialog-footer";
import "../../components/ha-formfield"; import "../../components/ha-formfield";
import "../../components/ha-switch"; import "../../components/ha-switch";
import "../../components/ha-button"; import "../../components/ha-button";
@@ -28,6 +29,8 @@ class DialogConfigEntrySystemOptions extends LitElement {
@state() private _submitting = false; @state() private _submitting = false;
@state() private _open = false;
public async showDialog( public async showDialog(
params: ConfigEntrySystemOptionsDialogParams params: ConfigEntrySystemOptionsDialogParams
): Promise<void> { ): Promise<void> {
@@ -35,9 +38,14 @@ class DialogConfigEntrySystemOptions extends LitElement {
this._error = undefined; this._error = undefined;
this._disableNewEntities = params.entry.pref_disable_new_entities; this._disableNewEntities = params.entry.pref_disable_new_entities;
this._disablePolling = params.entry.pref_disable_polling; this._disablePolling = params.entry.pref_disable_polling;
this._open = true;
} }
public closeDialog(): void { public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._error = ""; this._error = "";
this._params = undefined; this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -49,18 +57,19 @@ class DialogConfigEntrySystemOptions extends LitElement {
} }
return html` return html`
<ha-dialog <ha-wa-dialog
open .hass=${this.hass}
@closed=${this.closeDialog} .open=${this._open}
.heading=${createCloseHeading( header-title=${this.hass.localize(
this.hass, "ui.dialogs.config_entry_system_options.title",
this.hass.localize("ui.dialogs.config_entry_system_options.title", { {
integration: integration:
this.hass.localize( this.hass.localize(
`component.${this._params.entry.domain}.title` `component.${this._params.entry.domain}.title`
) || this._params.entry.domain, ) || this._params.entry.domain,
}) }
)} )}
@closed=${this._dialogClosed}
> >
${this._error ? html` <div class="error">${this._error}</div> ` : ""} ${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<ha-formfield <ha-formfield
@@ -82,10 +91,10 @@ class DialogConfigEntrySystemOptions extends LitElement {
</p>`} </p>`}
> >
<ha-switch <ha-switch
autofocus
.checked=${!this._disableNewEntities} .checked=${!this._disableNewEntities}
@change=${this._disableNewEntitiesChanged} @change=${this._disableNewEntitiesChanged}
.disabled=${this._submitting} .disabled=${this._submitting}
dialogInitialFocus
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
@@ -113,22 +122,27 @@ class DialogConfigEntrySystemOptions extends LitElement {
.disabled=${this._submitting} .disabled=${this._submitting}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
<ha-button
appearance="plain" <ha-dialog-footer slot="footer">
slot="primaryAction" <ha-button
@click=${this.closeDialog} appearance="plain"
.disabled=${this._submitting} slot="secondaryAction"
> @click=${this.closeDialog}
${this.hass.localize("ui.common.cancel")} .disabled=${this._submitting}
</ha-button> >
<ha-button ${this.hass.localize("ui.common.cancel")}
slot="primaryAction" </ha-button>
@click=${this._updateEntry} <ha-button
.disabled=${this._submitting} slot="primaryAction"
> @click=${this._updateEntry}
${this.hass.localize("ui.dialogs.config_entry_system_options.update")} .disabled=${this._submitting}
</ha-button> >
</ha-dialog> ${this.hass.localize(
"ui.dialogs.config_entry_system_options.update"
)}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`; `;
} }

View File

@@ -1,6 +1,5 @@
import { mdiOpenInNew } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; 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 { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-analytics"; import "../../../components/ha-analytics";
@@ -17,6 +16,8 @@ import {
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import { isDevVersion } from "../../../common/config/version";
import type { HaSwitch } from "../../../components/ha-switch";
@customElement("ha-config-analytics") @customElement("ha-config-analytics")
class ConfigAnalytics extends LitElement { class ConfigAnalytics extends LitElement {
@@ -34,10 +35,22 @@ class ConfigAnalytics extends LitElement {
: undefined; : undefined;
return html` return html`
<ha-card outlined> <ha-card
outlined
.header=${this.hass.localize("ui.panel.config.analytics.header") ||
"Home Assistant analytics"}
>
<div class="card-content"> <div class="card-content">
${error ? html`<div class="error">${error}</div>` : ""} ${error ? html`<div class="error">${error}</div>` : nothing}
<p>${this.hass.localize("ui.panel.config.analytics.intro")}</p> <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 <ha-analytics
translation_key_panel="config" translation_key_panel="config"
@analytics-preferences-changed=${this._preferencesChanged} @analytics-preferences-changed=${this._preferencesChanged}
@@ -45,26 +58,50 @@ class ConfigAnalytics extends LitElement {
.analytics=${this._analyticsDetails} .analytics=${this._analyticsDetails}
></ha-analytics> ></ha-analytics>
</div> </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> </ha-card>
<div class="footer"> ${isDevVersion(this.hass.config.version)
<ha-button ? html`<ha-card
size="small" outlined
appearance="plain" .header=${this.hass.localize(
href=${documentationUrl(this.hass, "/integrations/analytics/")} "ui.panel.config.analytics.preferences.snapshots.header"
target="_blank" )}
rel="noreferrer" >
> <div class="card-content">
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon> <p>
${this.hass.localize("ui.panel.config.analytics.learn_more")} ${this.hass.localize(
</ha-button> "ui.panel.config.analytics.preferences.snapshots.info"
</div> )}
<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 +133,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 { private _preferencesChanged(event: CustomEvent): void {
this._analyticsDetails = { this._analyticsDetails = {
...this._analyticsDetails!, ...this._analyticsDetails!,
preferences: event.detail.preferences, preferences: event.detail.preferences,
}; };
this._save();
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@@ -127,6 +178,9 @@ class ConfigAnalytics extends LitElement {
padding: 32px 0 16px; padding: 32px 0 16px;
text-align: center; text-align: center;
} }
ha-card:not(:first-of-type) {
margin-top: 24px;
}
ha-button[size="small"] ha-svg-icon { ha-button[size="small"] ha-svg-icon {
--mdc-icon-size: 16px; --mdc-icon-size: 16px;

View File

@@ -1,13 +1,11 @@
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state, query } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { mdiClose } from "@mdi/js";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-md-dialog"; import "../../../../components/ha-wa-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog"; import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-dialog-header"; import "../../../../components/ha-alert";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { LovelaceResourcesMutableParams } from "../../../../data/lovelace/resource"; import type { LovelaceResourcesMutableParams } from "../../../../data/lovelace/resource";
@@ -43,7 +41,7 @@ export class DialogLovelaceResourceDetail extends LitElement {
@state() private _submitting = false; @state() private _submitting = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog; @state() private _open = false;
public showDialog(params: LovelaceResourceDetailsDialogParams): void { public showDialog(params: LovelaceResourceDetailsDialogParams): void {
this._params = params; this._params = params;
@@ -58,6 +56,11 @@ export class DialogLovelaceResourceDetail extends LitElement {
url: "", url: "",
}; };
} }
this._open = true;
}
public closeDialog(): void {
this._open = false;
} }
private _dialogClosed(): void { private _dialogClosed(): void {
@@ -65,10 +68,6 @@ export class DialogLovelaceResourceDetail extends LitElement {
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
public closeDialog(): void {
this._dialog?.close();
}
protected render() { protected render() {
if (!this._params) { if (!this._params) {
return nothing; return nothing;
@@ -81,56 +80,45 @@ export class DialogLovelaceResourceDetail extends LitElement {
"ui.panel.config.lovelace.resources.detail.new_resource" "ui.panel.config.lovelace.resources.detail.new_resource"
); );
const ariaLabel = this._params.resource?.url
? this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.edit_resource"
)
: this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.new_resource"
);
return html` return html`
<ha-md-dialog <ha-wa-dialog
open .hass=${this.hass}
disable-cancel-action .open=${this._open}
prevent-scrim-close
header-title=${dialogTitle}
@closed=${this._dialogClosed} @closed=${this._dialogClosed}
.ariaLabel=${ariaLabel}
> >
<ha-dialog-header slot="headline"> <ha-alert
<ha-icon-button alert-type="warning"
slot="navigationIcon" .title=${this.hass!.localize(
.label=${this.hass.localize("ui.common.close") ?? "Close"} "ui.panel.config.lovelace.resources.detail.warning_header"
.path=${mdiClose} )}
@click=${this.closeDialog} >
></ha-icon-button> ${this.hass!.localize(
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span> "ui.panel.config.lovelace.resources.detail.warning_text"
</ha-dialog-header> )}
<div slot="content"> </ha-alert>
<ha-alert
alert-type="warning"
.title=${this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.warning_header"
)}
>
${this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.warning_text"
)}
</ha-alert>
<ha-form <ha-form
.schema=${this._schema(this._data)} autofocus
.data=${this._data} .schema=${this._schema(this._data)}
.hass=${this.hass} .data=${this._data}
.error=${this._error} .hass=${this.hass}
.computeLabel=${this._computeLabel} .error=${this._error}
@value-changed=${this._valueChanged} .computeLabel=${this._computeLabel}
></ha-form> @value-changed=${this._valueChanged}
</div> ></ha-form>
<div slot="actions">
<ha-button appearance="plain" @click=${this.closeDialog}> <ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this.hass!.localize("ui.common.cancel")} ${this.hass!.localize("ui.common.cancel")}
</ha-button> </ha-button>
<ha-button <ha-button
slot="primaryAction"
@click=${this._updateResource} @click=${this._updateResource}
.disabled=${urlInvalid || !this._data?.res_type || this._submitting} .disabled=${urlInvalid || !this._data?.res_type || this._submitting}
> >
@@ -142,8 +130,8 @@ export class DialogLovelaceResourceDetail extends LitElement {
"ui.panel.config.lovelace.resources.detail.create" "ui.panel.config.lovelace.resources.detail.create"
)} )}
</ha-button> </ha-button>
</div> </ha-dialog-footer>
</ha-md-dialog> </ha-wa-dialog>
`; `;
} }

View File

@@ -60,7 +60,7 @@ export class HuiEnergyDevicesGraphCard
state: true, state: true,
subscribe: false, subscribe: false,
}) })
private _chartType: "bar" | "pie" = "bar"; private _chartType?: "bar" | "pie";
@state() @state()
@storage({ @storage({
@@ -101,6 +101,14 @@ export class HuiEnergyDevicesGraphCard
this._config = config; this._config = config;
} }
private _getAllowedModes(): ("bar" | "pie")[] {
// Empty array or undefined = allow all modes
if (!this._config?.modes || this._config.modes.length === 0) {
return ["bar", "pie"];
}
return this._config.modes;
}
protected shouldUpdate(changedProps: PropertyValues): boolean { protected shouldUpdate(changedProps: PropertyValues): boolean {
return ( return (
hasConfigChanged(this, changedProps) || hasConfigChanged(this, changedProps) ||
@@ -109,8 +117,21 @@ export class HuiEnergyDevicesGraphCard
); );
} }
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("_config") && this._config) {
const allowedModes = this._getAllowedModes();
// If _chartType is not set or not in allowed modes, use first from config
if (!this._chartType || !allowedModes.includes(this._chartType)) {
this._chartType = allowedModes[0];
}
}
}
protected render() { protected render() {
if (!this.hass || !this._config) { if (!this.hass || !this._config || !this._chartType) {
return nothing; return nothing;
} }
@@ -118,13 +139,19 @@ export class HuiEnergyDevicesGraphCard
<ha-card> <ha-card>
<div class="card-header"> <div class="card-header">
<span>${this._config.title ? this._config.title : nothing}</span> <span>${this._config.title ? this._config.title : nothing}</span>
<ha-icon-button ${this._getAllowedModes().length > 1
.path=${this._chartType === "pie" ? mdiChartBar : mdiChartDonut} ? html`
.label=${this.hass.localize( <ha-icon-button
"ui.panel.lovelace.cards.energy.energy_devices_graph.change_chart_type" .path=${this._chartType === "pie"
)} ? mdiChartBar
@click=${this._handleChartTypeChange} : mdiChartDonut}
></ha-icon-button> .label=${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.change_chart_type"
)}
@click=${this._handleChartTypeChange}
></ha-icon-button>
`
: nothing}
</div> </div>
<div <div
class="content ${classMap({ class="content ${classMap({
@@ -529,7 +556,13 @@ export class HuiEnergyDevicesGraphCard
} }
private _handleChartTypeChange(): void { private _handleChartTypeChange(): void {
this._chartType = this._chartType === "pie" ? "bar" : "pie"; if (!this._chartType) {
return;
}
const allowedModes = this._getAllowedModes();
const currentIndex = allowedModes.indexOf(this._chartType);
const nextIndex = (currentIndex + 1) % allowedModes.length;
this._chartType = allowedModes[nextIndex];
this._getStatistics(this._data!); this._getStatistics(this._data!);
} }

View File

@@ -185,6 +185,7 @@ export interface EnergyDevicesGraphCardConfig extends EnergyCardBaseConfig {
title?: string; title?: string;
max_devices?: number; max_devices?: number;
hide_compound_stats?: boolean; hide_compound_stats?: boolean;
modes?: ("bar" | "pie")[];
} }
export interface EnergyDevicesDetailGraphCardConfig export interface EnergyDevicesDetailGraphCardConfig

View File

@@ -6760,6 +6760,7 @@
}, },
"analytics": { "analytics": {
"caption": "Analytics", "caption": "Analytics",
"header": "Home Assistant analytics",
"description": "Learn how to share data to improve Home Assistant", "description": "Learn how to share data to improve Home Assistant",
"preferences": { "preferences": {
"base": { "base": {
@@ -6777,10 +6778,17 @@
"diagnostics": { "diagnostics": {
"title": "Diagnostics", "title": "Diagnostics",
"description": "Share crash reports when unexpected errors occur." "description": "Share crash reports when unexpected errors occur."
},
"snapshots": {
"title": "Devices",
"description": "Generic information of your devices.",
"header": "Device analytics",
"info": "Anonymously share data about your devices to help build the Open Home Foundations 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", "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.", "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" "download_device_info": "Preview device analytics"
}, },