Compare commits

..

21 Commits

Author SHA1 Message Date
Petar Petrov
1d45c6c3fa show untracked energy in tooltip 2025-10-02 12:31:13 +03:00
Petar Petrov
9f40b344ec order compare data based on current data 2025-10-02 12:12:53 +03:00
Petar Petrov
046d87d828 handle label click in pie chart 2025-10-02 11:51:14 +03:00
Petar Petrov
165f265694 fix compare order 2025-10-02 11:43:13 +03:00
Petar Petrov
ec3c8b616f Merge branch 'dev' into energy-pie 2025-10-02 10:06:48 +03:00
karwosts
36daad1a10 Add a sub-editor to hui-entity-editor (#27157)
* Add a sub-editor to hui-entity-editor

* item styling
2025-10-02 10:05:59 +03:00
renovate[bot]
0d3315936e Update dependency @codemirror/view to v6.38.4 (#27288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 10:05:59 +03:00
renovate[bot]
2d6d0300b8 Update dependency lint-staged to v16.2.3 (#27285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 10:05:59 +03:00
Petar Petrov
3c15fc32b3 show untracked compound energy and total energy 2025-10-02 10:01:02 +03:00
karwosts
552691e200 Add a sub-editor to hui-entity-editor (#27157)
* Add a sub-editor to hui-entity-editor

* item styling
2025-10-02 08:19:24 +03:00
renovate[bot]
91258c86c1 Update dependency @codemirror/view to v6.38.4 (#27288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 08:06:29 +03:00
renovate[bot]
3750a378cd Update dependency lint-staged to v16.2.3 (#27285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 19:29:17 +02:00
Petar Petrov
3231316b27 Save chart type in storage 2025-10-01 16:34:43 +03:00
Petar Petrov
ed467991cf Add hide_compound_stats option to energy-devices-graph-card (#27263)
* Add hide_compound_stats option to energy-devices-graph-card

* Update src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* format

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-10-01 16:25:29 +03:00
Petar Petrov
c208431956 format 2025-10-01 16:25:03 +03:00
Petar Petrov
12d3304c72 Add hide_compound_stats option to energy-devices-graph-card (#27263)
* Add hide_compound_stats option to energy-devices-graph-card

* Update src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* format

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-10-01 16:24:10 +03:00
Petar Petrov
49f5512dd4 universal transition 2025-10-01 16:18:30 +03:00
Petar Petrov
aaa95266b6 Add pie chart mode to energy devices graph 2025-10-01 15:12:56 +03:00
renovate[bot]
246100809d Update dependency lint-staged to v16.2.2 (#27276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 12:38:43 +02:00
Michael Herger
6efca93186 Introduce new number formatting for Switzerland (#27268) 2025-10-01 11:08:25 +02:00
Simon Lamon
6280647b9a ha-refresh-tokens-card: Replace ha-button-menu to ha-md-button-menu (#26874) 2025-10-01 10:45:34 +02:00
20 changed files with 610 additions and 216 deletions

View File

@@ -34,7 +34,7 @@
"@codemirror/legacy-modes": "6.5.1",
"@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.38.3",
"@codemirror/view": "6.38.4",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.0",
@@ -203,7 +203,7 @@
"husky": "9.1.7",
"jsdom": "27.0.0",
"jszip": "3.10.1",
"lint-staged": "16.2.1",
"lint-staged": "16.2.3",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",

View File

@@ -32,6 +32,8 @@ export const numberFormatToLocale = (
return ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
case NumberFormat.space_comma:
return ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
case NumberFormat.quote_decimal:
return ["de-CH"]; // Use German (Switzerland) formatting 1'234'567.89
case NumberFormat.system:
return undefined;
default:

View File

@@ -22,7 +22,6 @@ import {
eventOptions,
property,
query,
queryAll,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -41,7 +40,7 @@ import { updateCanInstall } from "../data/update";
import { showEditSidebarDialog } from "../dialogs/sidebar/show-dialog-edit-sidebar";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleAnimations, haStyleScrollbar } from "../resources/styles";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types";
import "./ha-fade-in";
import "./ha-icon";
@@ -211,8 +210,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@query(".tooltip") private _tooltip!: HTMLDivElement;
@queryAll("ha-md-list-item") private _listItems!: NodeListOf<HaMdListItem>;
public hassSubscribe() {
return [
subscribeFrontendUserData(
@@ -325,15 +322,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
if (changedProps.has("alwaysExpand")) {
toggleAttribute(this, "expanded", this.alwaysExpand);
}
// Staggered animation for list items based on index
this._listItems.forEach((item, index) => {
(item as HTMLElement).style.setProperty(
"--animation-index",
String(index + 1)
);
});
if (!changedProps.has("hass")) {
return;
}
@@ -705,7 +693,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
static get styles(): CSSResultGroup {
return [
haStyleScrollbar,
haStyleAnimations,
css`
:host {
overflow: visible;
@@ -752,14 +739,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
.menu ha-icon-button {
color: var(--sidebar-icon-color);
animation: fadeInSlideDown var(--ha-animation-duration) ease-out both;
animation-delay: var(--ha-animation-delay-base) / 2;
}
ha-md-list-item {
animation: fadeInSlideDown var(--ha-animation-duration) ease-out both;
animation-delay: calc(
var(--ha-animation-delay-base) * var(--animation-index, 1) / 2
);
}
.title {
margin-left: 3px;
@@ -930,6 +909,11 @@ class HaSidebar extends SubscribeMixin(LitElement) {
padding: 4px;
font-weight: var(--ha-font-weight-medium);
}
.menu ha-icon-button {
-webkit-transform: scaleX(var(--scale-direction));
transform: scaleX(var(--scale-direction));
}
`,
];
}

View File

@@ -6,6 +6,7 @@ export enum NumberFormat {
system = "system",
comma_decimal = "comma_decimal",
decimal_comma = "decimal_comma",
quote_decimal = "quote_decimal",
space_comma = "space_comma",
none = "none",
}

View File

@@ -2,15 +2,21 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mdiChartDonut, mdiChartBar } from "@mdi/js";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import type { BarSeriesOption } from "echarts/charts";
import type { BarSeriesOption, PieSeriesOption } from "echarts/charts";
import { PieChart } from "echarts/charts";
import type { ECElementEvent } from "echarts/types/dist/shared";
import { getGraphColorByIndex } from "../../../../common/color/colors";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import type { EnergyData } from "../../../../data/energy";
import { getEnergyDataCollection } from "../../../../data/energy";
import {
computeConsumptionData,
getEnergyDataCollection,
getSummedData,
} from "../../../../data/energy";
import {
calculateStatisticSumGrowth,
getStatisticLabel,
@@ -25,6 +31,8 @@ import type { ECOption } from "../../../../resources/echarts";
import "../../../../components/ha-card";
import { fireEvent } from "../../../../common/dom/fire_event";
import { measureTextWidth } from "../../../../util/text";
import "../../../../components/ha-icon-button";
import { storage } from "../../../../common/decorators/storage";
@customElement("hui-energy-devices-graph-card")
export class HuiEnergyDevicesGraphCard
@@ -35,10 +43,20 @@ export class HuiEnergyDevicesGraphCard
@state() private _config?: EnergyDevicesGraphCardConfig;
@state() private _chartData: BarSeriesOption[] = [];
@state() private _chartData: (BarSeriesOption | PieSeriesOption)[] = [];
@state() private _data?: EnergyData;
@state()
@storage({
key: "energy-devices-graph-chart-type",
state: true,
subscribe: false,
})
private _chartType: "bar" | "pie" = "bar";
private _compoundStats: string[] = [];
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
@@ -75,9 +93,16 @@ export class HuiEnergyDevicesGraphCard
return html`
<ha-card>
${this._config.title
? html`<h1 class="card-header">${this._config.title}</h1>`
: ""}
<div class="card-header">
<span>${this._config.title ? this._config.title : nothing}</span>
<ha-icon-button
path=${this._chartType === "pie" ? mdiChartBar : mdiChartDonut}
label=${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.change_chart_type"
)}
@click=${this._handleChartTypeChange}
></ha-icon-button>
</div>
<div
class="content ${classMap({
"has-header": !!this._config.title,
@@ -86,9 +111,10 @@ export class HuiEnergyDevicesGraphCard
<ha-chart-base
.hass=${this.hass}
.data=${this._chartData}
.options=${this._createOptions(this._chartData)}
.height=${`${(this._chartData[0]?.data?.length || 0) * 28 + 50}px`}
.options=${this._createOptions(this._chartData, this._chartType)}
.height=${`${Math.max(300, (this._chartData[0]?.data?.length || 0) * 28 + 50)}px`}
@chart-click=${this._handleChartClick}
.extraComponents=${[PieChart]}
></ha-chart-base>
</div>
</ha-card>
@@ -97,71 +123,86 @@ export class HuiEnergyDevicesGraphCard
private _renderTooltip(params: any) {
const title = `<h4 style="text-align: center; margin: 0;">${this._getDeviceName(
params.value[1]
params.name
)}</h4>`;
const value = `${formatNumber(
params.value[0] as number,
this.hass.locale,
params.value[0] < 0.1 ? { maximumFractionDigits: 3 } : undefined
params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined
)} kWh`;
return `${title}${params.marker} ${params.seriesName}: ${value}`;
}
private _createOptions = memoizeOne((data: BarSeriesOption[]): ECOption => {
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
return {
xAxis: {
type: "value",
name: "kWh",
},
yAxis: {
type: "category",
inverse: true,
triggerEvent: true,
// take order from data
data: data[0]?.data?.map((d: any) => d.value[1]),
axisLabel: {
formatter: this._getDeviceName.bind(this),
overflow: "truncate",
fontSize: 12,
margin: 5,
width: Math.min(
isMobile ? 100 : 200,
Math.max(
...(data[0]?.data?.map(
(d: any) =>
measureTextWidth(this._getDeviceName(d.value[1]), 12) + 5
) || [])
)
),
private _createOptions = memoizeOne(
(
data: (BarSeriesOption | PieSeriesOption)[],
chartType: "bar" | "pie"
): ECOption => {
const options: ECOption = {
grid: {
top: 5,
left: 5,
right: 40,
bottom: 0,
containLabel: true,
},
},
grid: {
top: 5,
left: 5,
right: 40,
bottom: 0,
containLabel: true,
},
tooltip: {
show: true,
formatter: this._renderTooltip.bind(this),
},
};
});
tooltip: {
show: true,
formatter: this._renderTooltip.bind(this),
},
xAxis: { show: false },
yAxis: { show: false },
};
if (chartType === "bar") {
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
options.xAxis = {
show: true,
type: "value",
name: "kWh",
};
options.yAxis = {
show: true,
type: "category",
inverse: true,
triggerEvent: true,
// take order from data
data: data[0]?.data?.map((d: any) => d.name),
axisLabel: {
formatter: this._getDeviceName.bind(this),
overflow: "truncate",
fontSize: 12,
margin: 5,
width: Math.min(
isMobile ? 100 : 200,
Math.max(
...(data[0]?.data?.map(
(d: any) =>
measureTextWidth(this._getDeviceName(d.name), 12) + 5
) || [])
)
),
},
};
}
return options;
}
);
private _getDeviceName(statisticId: string): string {
const suffix = this._compoundStats.includes(statisticId)
? ` (${this.hass.localize("ui.panel.lovelace.cards.energy.energy_devices_graph.untracked")})`
: "";
return (
this._data?.prefs.device_consumption.find(
(this._data?.prefs.device_consumption.find(
(d) => d.stat_consumption === statisticId
)?.name ||
getStatisticLabel(
this.hass,
statisticId,
this._data?.statsMetadata[statisticId]
)
getStatisticLabel(
this.hass,
statisticId,
this._data?.statsMetadata[statisticId]
)) + suffix
);
}
@@ -169,51 +210,105 @@ export class HuiEnergyDevicesGraphCard
const data = energyData.stats;
const compareData = energyData.statsCompare;
const chartData: NonNullable<BarSeriesOption["data"]> = [];
const chartDataCompare: NonNullable<BarSeriesOption["data"]> = [];
const chartData: NonNullable<(BarSeriesOption | PieSeriesOption)["data"]> =
[];
const chartDataCompare: NonNullable<
(BarSeriesOption | PieSeriesOption)["data"]
> = [];
const datasets: BarSeriesOption[] = [
const datasets: (BarSeriesOption | PieSeriesOption)[] = [
{
type: "bar",
type: this._chartType,
radius: [compareData ? "50%" : "40%", "70%"],
universalTransition: true,
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.energy_usage"
),
itemStyle: {
borderRadius: [0, 4, 4, 0],
borderRadius: this._chartType === "bar" ? [0, 4, 4, 0] : 4,
},
data: chartData,
barWidth: compareData ? 10 : 20,
cursor: "default",
},
minShowLabelAngle: 15,
label:
this._chartType === "pie"
? {
formatter: ({ name }) => this._getDeviceName(name),
}
: undefined,
} as BarSeriesOption | PieSeriesOption,
];
if (compareData) {
datasets.push({
type: "bar",
type: this._chartType,
radius: ["30%", "50%"],
universalTransition: true,
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.previous_energy_usage"
),
itemStyle: {
borderRadius: [0, 4, 4, 0],
borderRadius: this._chartType === "bar" ? [0, 4, 4, 0] : 4,
},
data: chartDataCompare,
barWidth: 10,
cursor: "default",
});
label: this._chartType === "pie" ? { show: false } : undefined,
emphasis:
this._chartType === "pie"
? {
focus: "series",
blurScope: "global",
}
: undefined,
} as BarSeriesOption | PieSeriesOption);
}
const computedStyle = getComputedStyle(this);
energyData.prefs.device_consumption.forEach((device, id) => {
const value =
this._compoundStats = energyData.prefs.device_consumption
.map((d) => d.included_in_stat)
.filter(Boolean) as string[];
const devices = energyData.prefs.device_consumption;
const devicesTotals: Record<string, number> = {};
devices.forEach((device) => {
devicesTotals[device.stat_consumption] =
device.stat_consumption in data
? calculateStatisticSumGrowth(data[device.stat_consumption]) || 0
: 0;
const color = getGraphColorByIndex(id, computedStyle);
});
const devicesTotalsCompare: Record<string, number> = {};
if (compareData) {
devices.forEach((device) => {
devicesTotalsCompare[device.stat_consumption] =
device.stat_consumption in compareData
? calculateStatisticSumGrowth(
compareData[device.stat_consumption]
) || 0
: 0;
});
}
devices.forEach((device, idx) => {
let value = devicesTotals[device.stat_consumption];
if (!this._config?.hide_compound_stats) {
const childSum = devices.reduce((acc, d) => {
if (d.included_in_stat === device.stat_consumption) {
return acc + devicesTotals[d.stat_consumption];
}
return acc;
}, 0);
value -= Math.min(value, childSum);
} else if (this._compoundStats.includes(device.stat_consumption)) {
return;
}
const color = getGraphColorByIndex(idx, computedStyle);
chartData.push({
id,
value: [value, device.stat_consumption],
id: device.stat_consumption,
value: [value, device.stat_consumption] as any,
name: device.stat_consumption,
itemStyle: {
color: color + "7F",
borderColor: color,
@@ -221,16 +316,24 @@ export class HuiEnergyDevicesGraphCard
});
if (compareData) {
const compareValue =
let compareValue =
device.stat_consumption in compareData
? calculateStatisticSumGrowth(
compareData[device.stat_consumption]
) || 0
: 0;
const compareChildSum = devices.reduce((acc, d) => {
if (d.included_in_stat === device.stat_consumption) {
return acc + devicesTotalsCompare[d.stat_consumption];
}
return acc;
}, 0);
compareValue -= Math.min(compareValue, compareChildSum);
chartDataCompare.push({
id,
value: [compareValue, device.stat_consumption],
id: device.stat_consumption,
value: [compareValue, device.stat_consumption] as any,
name: device.stat_consumption,
itemStyle: {
color: color + "32",
borderColor: color + "7F",
@@ -240,11 +343,62 @@ export class HuiEnergyDevicesGraphCard
});
chartData.sort((a: any, b: any) => b.value[0] - a.value[0]);
if (compareData) {
datasets[1].data = chartData.map((d) =>
chartDataCompare.find((d2) => (d2 as any).id === d.id)
) as typeof chartDataCompare;
}
chartData.length = Math.min(
this._config?.max_devices || Infinity,
chartData.length
);
datasets.forEach((dataset) => {
dataset.data!.length = Math.min(
this._config?.max_devices || Infinity,
dataset.data!.length
);
});
if (this._chartType === "pie") {
const { summedData } = getSummedData(energyData);
const { consumption } = computeConsumptionData(summedData);
const totalUsed = consumption.total.used_total;
const showUntracked =
"from_grid" in summedData ||
"solar" in summedData ||
"from_battery" in summedData;
const untracked = showUntracked
? totalUsed -
chartData.reduce((acc: number, d: any) => acc + d.value[0], 0)
: 0;
datasets.push({
type: "pie",
radius: ["0%", compareData ? "30%" : "40%"],
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.total_energy_usage"
),
data: [totalUsed],
label: {
show: true,
position: "center",
color: computedStyle.getPropertyValue("--secondary-text-color"),
fontSize: computedStyle.getPropertyValue("--ha-font-size-l"),
lineHeight: 24,
fontWeight: "bold",
formatter: `{a}\n${formatNumber(totalUsed, this.hass.locale)} kWh`,
},
cursor: "default",
itemStyle: {
color: "rgba(0, 0, 0, 0)",
},
tooltip: {
formatter: () =>
untracked > 0
? this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.includes_untracked",
{ num: formatNumber(untracked, this.hass.locale) }
)
: "",
},
});
}
this._chartData = datasets;
await this.updateComplete;
@@ -259,11 +413,26 @@ export class HuiEnergyDevicesGraphCard
fireEvent(this, "hass-more-info", {
entityId: e.detail.value as string,
});
} else if (
e.detail.seriesType === "pie" &&
e.detail.event?.target?.type === "tspan" // label
) {
fireEvent(this, "hass-more-info", {
entityId: (e.detail.data as any).id as string,
});
}
}
private _handleChartTypeChange(): void {
this._chartType = this._chartType === "pie" ? "bar" : "pie";
this._getStatistics(this._data!);
}
static styles = css`
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 0;
}
.content {
@@ -275,6 +444,11 @@ export class HuiEnergyDevicesGraphCard
ha-chart-base {
--chart-max-height: none;
}
ha-icon-button {
transform: rotate(90deg);
color: var(--secondary-text-color);
cursor: pointer;
}
`;
}

View File

@@ -176,6 +176,7 @@ export interface EnergyDevicesGraphCardConfig extends EnergyCardBaseConfig {
type: "energy-devices-graph";
title?: string;
max_devices?: number;
hide_compound_stats?: boolean;
}
export interface EnergyDevicesDetailGraphCardConfig
@@ -282,7 +283,7 @@ export interface GlanceConfigEntity extends ConfigEntity {
image?: string;
show_state?: boolean;
state_color?: boolean;
format: TimestampRenderingFormat;
format?: TimestampRenderingFormat;
}
export interface GlanceCardConfig extends LovelaceCardConfig {

View File

@@ -1,4 +1,4 @@
import { mdiDrag } from "@mdi/js";
import { mdiClose, mdiDrag, mdiPencil } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
@@ -12,6 +12,7 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-sortable";
import type { HomeAssistant } from "../../../types";
import type { EntityConfig } from "../entity-rows/types";
import { computeRTL } from "../../../common/util/compute_rtl";
@customElement("hui-entity-editor")
export class HuiEntityEditor extends LitElement {
@@ -24,6 +25,8 @@ export class HuiEntityEditor extends LitElement {
@property() public label?: string;
@property({ attribute: "can-edit", type: Boolean }) public canEdit?;
private _entityKeys = new WeakMap<EntityConfig, string>();
private _getKey(action: EntityConfig) {
@@ -34,6 +37,70 @@ export class HuiEntityEditor extends LitElement {
return this._entityKeys.get(action)!;
}
private _renderItem(item: EntityConfig, index: number) {
const stateObj = this.hass!.states[item.entity];
const entityName =
stateObj && this.hass!.formatEntityName(stateObj, "entity");
const deviceName =
stateObj && this.hass!.formatEntityName(stateObj, "device");
const areaName = stateObj && this.hass!.formatEntityName(stateObj, "area");
const isRTL = computeRTL(this.hass!);
const primary = item.name || entityName || deviceName || item.entity;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
return html`
<ha-md-list-item class="item">
<ha-svg-icon class="handle" .path=${mdiDrag} slot="start"></ha-svg-icon>
<div slot="headline" class="label">${primary}</div>
${secondary
? html`<div slot="supporting-text" class="description">
${secondary}
</div>`
: nothing}
<ha-icon-button
slot="end"
.item=${item}
.index=${index}
.label=${this.hass!.localize("ui.common.edit")}
.path=${mdiPencil}
@click=${this._editItem}
></ha-icon-button>
<ha-icon-button
slot="end"
.index=${index}
.label=${this.hass!.localize("ui.common.delete")}
.path=${mdiClose}
@click=${this._deleteItem}
></ha-icon-button>
</ha-md-list-item>
`;
}
private _editItem(ev) {
const index = (ev.currentTarget as any).index;
fireEvent(this, "edit-detail-element", {
subElementConfig: {
index,
type: "row",
elementConfig: this.entities![index],
},
});
}
private _deleteItem(ev) {
const index = ev.target.index;
const newConfigEntities = this.entities!.slice(0, index).concat(
this.entities!.slice(index + 1)
);
fireEvent(this, "entities-changed", { entities: newConfigEntities });
}
protected render() {
if (!this.entities) {
return nothing;
@@ -47,29 +114,48 @@ export class HuiEntityEditor extends LitElement {
this.hass!.localize("ui.panel.lovelace.editor.card.config.required") +
")"}
</h3>
<ha-sortable handle-selector=".handle" @item-moved=${this._entityMoved}>
<div class="entities">
${repeat(
this.entities,
(entityConf) => this._getKey(entityConf),
(entityConf, index) => html`
<div class="entity" data-entity-id=${entityConf.entity}>
<div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
<ha-entity-picker
.hass=${this.hass}
.value=${entityConf.entity}
.index=${index}
.entityFilter=${this.entityFilter}
@value-changed=${this._valueChanged}
allow-custom-entity
></ha-entity-picker>
</div>
`
)}
</div>
</ha-sortable>
${this.canEdit
? html`
<div class="items-container">
<ha-sortable
handle-selector=".handle"
draggable-selector=".item"
@item-moved=${this._entityMoved}
>
<ha-md-list>
${this.entities.map((item, index) =>
this._renderItem(item, index)
)}
</ha-md-list>
</ha-sortable>
</div>
`
: html` <ha-sortable
handle-selector=".handle"
@item-moved=${this._entityMoved}
>
<div class="entities">
${repeat(
this.entities,
(entityConf) => this._getKey(entityConf),
(entityConf, index) => html`
<div class="entity" data-entity-id=${entityConf.entity}>
<div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
<ha-entity-picker
.hass=${this.hass}
.value=${entityConf.entity}
.index=${index}
.entityFilter=${this.entityFilter}
@value-changed=${this._valueChanged}
allow-custom-entity
></ha-entity-picker>
</div>
`
)}
</div>
</ha-sortable>`}
<ha-entity-picker
class="add-entity"
.hass=${this.hass}
@@ -148,6 +234,35 @@ export class HuiEntityEditor extends LitElement {
.entity ha-entity-picker {
flex-grow: 1;
}
ha-md-list {
gap: 8px;
}
ha-md-list-item {
border: 1px solid var(--divider-color);
border-radius: 8px;
--ha-md-list-item-gap: 0;
--md-list-item-top-space: 0;
--md-list-item-bottom-space: 0;
--md-list-item-leading-space: 12px;
--md-list-item-trailing-space: 4px;
--md-list-item-two-line-container-height: 48px;
--md-list-item-one-line-container-height: 48px;
}
.handle {
cursor: move;
padding: 8px;
margin-inline-start: -8px;
}
label {
margin-bottom: 8px;
display: block;
}
ha-md-list-item .label,
ha-md-list-item .description {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
`;
}

View File

@@ -31,6 +31,8 @@ export class HuiGenericEntityRowEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public schema?;
@state() private _config?: EntitiesCardEntityConfig;
public setConfig(config: EntitiesCardEntityConfig): void {
@@ -87,7 +89,8 @@ export class HuiGenericEntityRowEditor
return nothing;
}
const schema = this._schema(this._config.entity, this.hass.localize);
const schema =
this.schema || this._schema(this._config.entity, this.hass.localize);
return html`
<ha-form

View File

@@ -13,6 +13,9 @@ import {
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import "../hui-sub-element-editor";
import type { EditDetailElementEvent, SubElementEditorConfig } from "../types";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { ConfigEntity, GlanceCardConfig } from "../../cards/types";
@@ -21,6 +24,7 @@ import type { LovelaceCardEditor } from "../../types";
import { processEditorEntities } from "../process-editor-entities";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entitiesConfigStruct } from "../structs/entities-struct";
import type { EntityConfig } from "../../entity-rows/types";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -36,6 +40,49 @@ const cardConfigStruct = assign(
})
);
const SUB_SCHEMA = [
{ name: "entity", selector: { entity: {} }, required: true },
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{
name: "icon",
selector: {
icon: {},
},
context: {
icon_entity: "entity",
},
},
{ name: "show_last_changed", selector: { boolean: {} } },
{ name: "show_state", selector: { boolean: {} }, default: true },
],
},
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map((action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})),
},
] as const;
const SCHEMA = [
{ name: "title", selector: { text: {} } },
{
@@ -68,6 +115,8 @@ export class HuiGlanceCardEditor
@state() private _config?: GlanceCardConfig;
@state() private _subElementEditorConfig?: SubElementEditorConfig;
@state() private _configEntities?: ConfigEntity[];
public setConfig(config: GlanceCardConfig): void {
@@ -81,6 +130,19 @@ export class HuiGlanceCardEditor
return nothing;
}
if (this._subElementEditorConfig) {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.schema=${SUB_SCHEMA}
@go-back=${this._goBack}
@config-changed=${this._handleSubEntityChanged}
>
</hui-sub-element-editor>
`;
}
const data = {
show_name: true,
show_icon: true,
@@ -98,12 +160,42 @@ export class HuiGlanceCardEditor
></ha-form>
<hui-entity-editor
.hass=${this.hass}
can-edit
.entities=${this._configEntities}
@entities-changed=${this._entitiesChanged}
@edit-detail-element=${this._editDetailElement}
></hui-entity-editor>
`;
}
private _goBack(): void {
this._subElementEditorConfig = undefined;
}
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
this._subElementEditorConfig = ev.detail.subElementConfig;
}
private _handleSubEntityChanged(ev: CustomEvent): void {
ev.stopPropagation();
const index = this._subElementEditorConfig!.index!;
const newEntities = this._configEntities!.concat();
const newConfig = ev.detail.config as EntityConfig;
this._subElementEditorConfig = {
...this._subElementEditorConfig!,
elementConfig: newConfig,
};
newEntities[index] = newConfig;
let config = this._config!;
config = { ...config, entities: newEntities };
this._config = config;
this._configEntities = processEditorEntities(config.entities);
fireEvent(this, "config-changed", { config });
}
private _valueChanged(ev: CustomEvent): void {
const config = ev.detail.value;
fireEvent(this, "config-changed", { config });

View File

@@ -18,6 +18,9 @@ import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { HistoryGraphCardConfig } from "../../cards/types";
import "../../components/hui-entity-editor";
import "../hui-sub-element-editor";
import type { EditDetailElementEvent, SubElementEditorConfig } from "../types";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import type { EntityConfig } from "../../entity-rows/types";
import type { LovelaceCardEditor } from "../../types";
import { processEditorEntities } from "../process-editor-entities";
@@ -40,6 +43,11 @@ const cardConfigStruct = assign(
})
);
const SUB_SCHEMA = [
{ name: "entity", selector: { entity: {} }, required: true },
{ name: "name", selector: { text: {} } },
] as const;
@customElement("hui-history-graph-card-editor")
export class HuiHistoryGraphCardEditor
extends LitElement
@@ -49,6 +57,8 @@ export class HuiHistoryGraphCardEditor
@state() private _config?: HistoryGraphCardConfig;
@state() private _subElementEditorConfig?: SubElementEditorConfig;
@state() private _configEntities?: EntityConfig[];
public setConfig(config: HistoryGraphCardConfig): void {
@@ -110,6 +120,19 @@ export class HuiHistoryGraphCardEditor
return nothing;
}
if (this._subElementEditorConfig) {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.schema=${SUB_SCHEMA}
@go-back=${this._goBack}
@config-changed=${this._handleSubEntityChanged}
>
</hui-sub-element-editor>
`;
}
const schema = this._schema(
this._config!.min_y_axis !== undefined ||
this._config!.max_y_axis !== undefined
@@ -126,11 +149,41 @@ export class HuiHistoryGraphCardEditor
<hui-entity-editor
.hass=${this.hass}
.entities=${this._configEntities}
can-edit
@entities-changed=${this._entitiesChanged}
@edit-detail-element=${this._editDetailElement}
></hui-entity-editor>
`;
}
private _goBack(): void {
this._subElementEditorConfig = undefined;
}
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
this._subElementEditorConfig = ev.detail.subElementConfig;
}
private _handleSubEntityChanged(ev: CustomEvent): void {
ev.stopPropagation();
const index = this._subElementEditorConfig!.index!;
const newEntities = this._configEntities!.concat();
const newConfig = ev.detail.config as EntityConfig;
this._subElementEditorConfig = {
...this._subElementEditorConfig!,
elementConfig: newConfig,
};
newEntities[index] = newConfig;
let config = this._config!;
config = { ...config, entities: newEntities };
this._config = config;
this._configEntities = processEditorEntities(config.entities);
fireEvent(this, "config-changed", { config });
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}

View File

@@ -57,6 +57,8 @@ export abstract class HuiElementEditor<
@property({ attribute: false }) public context?: C;
@property({ attribute: false }) public schema?;
@state() private _config?: T;
@state() private _configElement?: LovelaceGenericElementEditor;
@@ -312,6 +314,9 @@ export abstract class HuiElementEditor<
if (this._configElement && changedProperties.has("context")) {
this._configElement.context = this.context;
}
if (this._configElement && changedProperties.has("schema")) {
this._configElement.schema = this.schema;
}
}
private _handleUIConfigChanged(ev: UIConfigChangedEvent<T>) {
@@ -399,6 +404,7 @@ export abstract class HuiElementEditor<
configElement.lovelace = this.lovelace;
}
configElement.context = this.context;
configElement.schema = this.schema;
configElement.addEventListener("config-changed", (ev) =>
this._handleUIConfigChanged(ev as UIConfigChangedEvent<T>)
);

View File

@@ -27,6 +27,8 @@ export class HuiSubElementEditor extends LitElement {
@property({ attribute: false }) public config!: SubElementEditorConfig;
@property({ attribute: false }) public schema?;
@state() private _guiModeAvailable = true;
@state() private _guiMode = true;
@@ -89,6 +91,7 @@ export class HuiSubElementEditor extends LitElement {
.hass=${this.hass}
.value=${this.config.elementConfig}
.context=${this.config.context}
.schema=${this.schema}
@config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged}
></hui-row-element-editor>

View File

@@ -18,6 +18,8 @@ export const entitiesConfigStruct = union([
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
confirmation: optional(actionConfigStructConfirmation),
show_last_changed: optional(boolean()),
show_state: optional(boolean()),
}),
string(),
]);

View File

@@ -72,7 +72,7 @@ import {
} from "../../dialogs/quick-bar/show-dialog-quick-bar";
import { showShortcutsDialog } from "../../dialogs/shortcuts/show-shortcuts-dialog";
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { haStyle, haStyleAnimations } from "../../resources/styles";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, PanelInfo } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { showToast } from "../../util/toast";
@@ -1205,7 +1205,6 @@ class HUIRoot extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleAnimations,
css`
:host {
-ms-user-select: none;
@@ -1260,8 +1259,6 @@ class HUIRoot extends LitElement {
padding: 0px 12px;
font-weight: var(--ha-font-weight-normal);
box-sizing: border-box;
animation: fadeIn var(--ha-animation-duration) ease-out both;
animation-delay: var(--ha-animation-delay-base);
}
.narrow .toolbar {
padding: 0 4px;
@@ -1410,8 +1407,6 @@ class HUIRoot extends LitElement {
hui-view-container > * {
flex: 1 1 100%;
max-width: 100%;
animation: fadeInSlideDown var(--ha-animation-duration) ease-out both;
animation-delay: var(--ha-animation-delay-base);
}
/**
* In edit mode we have the tab bar on a new line *

View File

@@ -169,6 +169,7 @@ export interface LovelaceGenericElementEditor<C = any> extends HTMLElement {
hass?: HomeAssistant;
lovelace?: LovelaceConfig;
context?: C;
schema?: any;
setConfig(config: any): void;
focusYamlEditor?: () => void;
}

View File

@@ -1,4 +1,3 @@
import type { ActionDetail } from "@material/mwc-list";
import {
mdiAndroid,
mdiApple,
@@ -15,7 +14,8 @@ import memoizeOne from "memoize-one";
import { relativeTime } from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import "../../components/ha-button-menu";
import "../../components/ha-md-button-menu";
import "../../components/ha-md-menu-item";
import "../../components/ha-card";
import "../../components/ha-icon-button";
import "../../components/ha-label";
@@ -146,20 +146,19 @@ class HaRefreshTokens extends LitElement {
)}
</div>
<div>
<ha-button-menu
corner="BOTTOM_END"
menu-corner="END"
@action=${this._handleAction}
.token=${token}
>
<ha-md-button-menu positioning="popover">
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon">
<ha-md-menu-item
graphic="icon"
@click=${this._toggleTokenExpiration}
.token=${token}
>
<ha-svg-icon
slot="graphic"
slot="start"
.path=${token.expire_at
? mdiClockRemoveOutline
: mdiClockCheckOutline}
@@ -171,20 +170,24 @@ class HaRefreshTokens extends LitElement {
: this.hass.localize(
"ui.panel.profile.refresh_tokens.enable_token_expiration"
)}
</ha-list-item>
<ha-list-item
</ha-md-menu-item>
<ha-md-menu-item
graphic="icon"
class="warning"
.disabled=${token.is_current}
@click=${this._deleteToken}
.token=${token}
>
<ha-svg-icon
class="warning"
slot="graphic"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
${this.hass.localize("ui.common.delete")}
</ha-list-item>
</ha-button-menu>
<div slot="headline">
${this.hass.localize("ui.common.delete")}
</div>
</ha-md-menu-item>
</ha-md-button-menu>
</div>
</ha-settings-row>
`
@@ -207,19 +210,8 @@ class HaRefreshTokens extends LitElement {
`;
}
private async _handleAction(ev: CustomEvent<ActionDetail>) {
const token = (ev.currentTarget as any).token;
switch (ev.detail.index) {
case 0:
this._toggleTokenExpiration(token);
break;
case 1:
this._deleteToken(token);
break;
}
}
private async _toggleTokenExpiration(token: RefreshToken): Promise<void> {
private async _toggleTokenExpiration(ev): Promise<void> {
const token = (ev.currentTarget as any).token as RefreshToken;
const enable = !token.expire_at;
if (!enable) {
if (
@@ -260,7 +252,8 @@ class HaRefreshTokens extends LitElement {
}
}
private async _deleteToken(token: RefreshToken): Promise<void> {
private async _deleteToken(ev): Promise<void> {
const token = (ev.currentTarget as any).token as RefreshToken;
if (
!(await showConfirmationDialog(this, {
title: this.hass.localize(
@@ -332,8 +325,8 @@ class HaRefreshTokens extends LitElement {
ha-icon-button {
color: var(--primary-text-color);
}
ha-list-item[disabled],
ha-list-item[disabled] ha-svg-icon {
ha-md-list-item[disabled],
ha-md-list-item[disabled] ha-svg-icon {
color: var(--disabled-text-color) !important;
}
ha-settings-row .current-session {

View File

@@ -195,36 +195,3 @@ export const baseEntrypointStyles = css`
width: 100vw;
}
`;
export const haStyleAnimations = css`
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeInSlideUp {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInSlideDown {
0% {
opacity: 0;
transform: translateY(-20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
`;

View File

@@ -42,10 +42,6 @@ export const coreStyles = css`
--ha-space-18: 72px;
--ha-space-19: 76px;
--ha-space-20: 80px;
/* Animation timing */
--ha-animation-duration: 400ms;
--ha-animation-delay-base: 50ms;
}
`;

View File

@@ -7025,7 +7025,11 @@
},
"energy_devices_graph": {
"energy_usage": "Energy usage",
"previous_energy_usage": "Previous energy usage"
"previous_energy_usage": "Previous energy usage",
"total_energy_usage": "Total energy usage",
"change_chart_type": "Change chart type",
"untracked": "untracked",
"includes_untracked": "Includes {num} kWh of untracked energy"
},
"energy_devices_detail_graph": {
"untracked_consumption": "Untracked consumption",
@@ -7720,6 +7724,7 @@
"show_icon": "Show icon",
"show_name": "Show name",
"show_state": "Show state",
"show_last_changed": "Show last changed",
"tap_action": "Tap behavior",
"interactions": "Interactions",
"title": "Title",
@@ -8440,6 +8445,7 @@
"system": "Use system locale",
"comma_decimal": "1,234,567.89",
"decimal_comma": "1.234.567,89",
"quote_decimal": "1'234'567.89",
"space_comma": "1234567,89",
"none": "None"
}

View File

@@ -1284,15 +1284,15 @@ __metadata:
languageName: node
linkType: hard
"@codemirror/view@npm:6.38.3, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
version: 6.38.3
resolution: "@codemirror/view@npm:6.38.3"
"@codemirror/view@npm:6.38.4, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
version: 6.38.4
resolution: "@codemirror/view@npm:6.38.4"
dependencies:
"@codemirror/state": "npm:^6.5.0"
crelt: "npm:^1.0.6"
style-mod: "npm:^4.1.0"
w3c-keyname: "npm:^2.2.4"
checksum: 10/2df41450399cbac0eaf06dba822418dd6926e48344b9255902248075ef040c957dfe97fe842a755e284a2fd4a66dc17b9638385f46ad74e926baac2e797335a2
checksum: 10/86b3894e9e7c2113aabb1db8684d0520378339c194fa56a688fc26cd7d40336bb9df1f5f19f68309d95f14b80ecf0b70c0ffe5e43f2ec11c4bab18f2d5ee4494
languageName: node
linkType: hard
@@ -9187,7 +9187,7 @@ __metadata:
"@codemirror/legacy-modes": "npm:6.5.1"
"@codemirror/search": "npm:6.5.11"
"@codemirror/state": "npm:6.5.2"
"@codemirror/view": "npm:6.38.3"
"@codemirror/view": "npm:6.38.4"
"@date-fns/tz": "npm:1.4.1"
"@egjs/hammerjs": "npm:2.0.17"
"@formatjs/intl-datetimeformat": "npm:6.18.0"
@@ -9322,7 +9322,7 @@ __metadata:
leaflet: "npm:1.9.4"
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"
lint-staged: "npm:16.2.1"
lint-staged: "npm:16.2.3"
lit: "npm:3.3.1"
lit-analyzer: "npm:2.0.3"
lit-html: "npm:3.3.1"
@@ -10657,9 +10657,9 @@ __metadata:
languageName: node
linkType: hard
"lint-staged@npm:16.2.1":
version: 16.2.1
resolution: "lint-staged@npm:16.2.1"
"lint-staged@npm:16.2.3":
version: 16.2.3
resolution: "lint-staged@npm:16.2.3"
dependencies:
commander: "npm:^14.0.1"
listr2: "npm:^9.0.4"
@@ -10670,7 +10670,7 @@ __metadata:
yaml: "npm:^2.8.1"
bin:
lint-staged: bin/lint-staged.js
checksum: 10/b604de3ca27a067e45c5f0e0780ca46f5617e9f6ac3895297dee087d62742bbcd9f9e910300c15c599e1f06900666469b73e036e3fe3153ccedef314ce791dd5
checksum: 10/7c83cb478aa8004eecc8c91d633abe2865ffc037957ae9ee2669e49b76b76fe3512ba431277efc29cec7a38641e7d8a62f3378a41b624c88bde6fbef5524e2cb
languageName: node
linkType: hard