mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-05 00:49:53 +00:00
Compare commits
73 Commits
copilot/al
...
20251029.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f5875c30f | ||
|
|
517cd49f35 | ||
|
|
25d9fc94b2 | ||
|
|
7b188759e3 | ||
|
|
76772d1098 | ||
|
|
6052745ca0 | ||
|
|
89b9780345 | ||
|
|
a607edca96 | ||
|
|
52eb3d8063 | ||
|
|
3e749ec085 | ||
|
|
ee2ec00069 | ||
|
|
0aa2941868 | ||
|
|
46cd1d5156 | ||
|
|
07a5c41fd4 | ||
|
|
4ad3c553d5 | ||
|
|
d40cc448a5 | ||
|
|
e2f3f9d348 | ||
|
|
98d44950f8 | ||
|
|
8ae9edb1ef | ||
|
|
84c4396c13 | ||
|
|
2b937a30e3 | ||
|
|
b7815bfd86 | ||
|
|
d94fa03411 | ||
|
|
0a7007ef9e | ||
|
|
dd12136dee | ||
|
|
6e2f89fe3d | ||
|
|
092085b9af | ||
|
|
1c06eb8661 | ||
|
|
c7e87b06b5 | ||
|
|
38c738c199 | ||
|
|
e899587307 | ||
|
|
c9feb0b75f | ||
|
|
10718c35d1 | ||
|
|
4dc6a37bad | ||
|
|
ac49fc7aba | ||
|
|
e4f008800b | ||
|
|
0b0ffd7bab | ||
|
|
dfa77526a2 | ||
|
|
9a3bd6c613 | ||
|
|
1161de5746 | ||
|
|
9df8e20391 | ||
|
|
11047a9c95 | ||
|
|
18fa66f61c | ||
|
|
758a048f34 | ||
|
|
ee0fc360b0 | ||
|
|
4012f95ec1 | ||
|
|
0336ce4606 | ||
|
|
9ba36ab7e2 | ||
|
|
fe7a08a1b0 | ||
|
|
87a8f9cedc | ||
|
|
01df7e20ca | ||
|
|
d181219522 | ||
|
|
6ae24b8135 | ||
|
|
8e009f24f9 | ||
|
|
53031f44ac | ||
|
|
af5a988457 | ||
|
|
bab0391a19 | ||
|
|
444123c47e | ||
|
|
f123d34046 | ||
|
|
1b40f99f68 | ||
|
|
b314b3ed2b | ||
|
|
59b8932969 | ||
|
|
107af753ec | ||
|
|
1f0acb3046 | ||
|
|
431e533929 | ||
|
|
02c845cbc6 | ||
|
|
628111ed20 | ||
|
|
e825a9c02f | ||
|
|
7a35bddf36 | ||
|
|
ad69270af8 | ||
|
|
404edf9483 | ||
|
|
a166b4e9b6 | ||
|
|
7a285f11db |
@@ -235,5 +235,8 @@
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.10.3"
|
||||
"packageManager": "yarn@4.10.3",
|
||||
"volta": {
|
||||
"node": "22.21.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20250924.0"
|
||||
version = "20251029.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -35,6 +35,7 @@ export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||
const LEGEND_OVERFLOW_LIMIT = 10;
|
||||
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
|
||||
const DOUBLE_TAP_TIME = 300;
|
||||
const RESIZE_ANIMATION_DURATION = 250;
|
||||
|
||||
export type CustomLegendOption = ECOption["legend"] & {
|
||||
type: "custom";
|
||||
@@ -205,6 +206,15 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
if (changedProps.has("options")) {
|
||||
chartOptions = { ...chartOptions, ...this._createOptions() };
|
||||
if (
|
||||
this._compareCustomLegendOptions(
|
||||
changedProps.get("options"),
|
||||
this.options
|
||||
)
|
||||
) {
|
||||
// custom legend changes may require a resize to layout properly
|
||||
this._shouldResizeChart = true;
|
||||
}
|
||||
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
|
||||
chartOptions.dataZoom = this._getDataZoomConfig();
|
||||
}
|
||||
@@ -296,7 +306,7 @@ export class HaChartBase extends LitElement {
|
||||
itemStyle = {
|
||||
color: dataset?.color as string,
|
||||
...(dataset?.itemStyle as { borderColor?: string }),
|
||||
itemStyle,
|
||||
...itemStyle,
|
||||
};
|
||||
const color = itemStyle?.color as string;
|
||||
const borderColor = itemStyle?.borderColor as string;
|
||||
@@ -508,6 +518,7 @@ export class HaChartBase extends LitElement {
|
||||
);
|
||||
}
|
||||
});
|
||||
this.requestUpdate("_hiddenDatasets");
|
||||
}
|
||||
|
||||
private _getDataZoomConfig(): DataZoomComponentOption | undefined {
|
||||
@@ -958,11 +969,31 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _handleChartRenderFinished = () => {
|
||||
if (this._shouldResizeChart) {
|
||||
this.chart?.resize();
|
||||
this.chart?.resize({
|
||||
animation: this._reducedMotion
|
||||
? undefined
|
||||
: { duration: RESIZE_ANIMATION_DURATION },
|
||||
});
|
||||
this._shouldResizeChart = false;
|
||||
}
|
||||
};
|
||||
|
||||
private _compareCustomLegendOptions(
|
||||
oldOptions: ECOption | undefined,
|
||||
newOptions: ECOption | undefined
|
||||
): boolean {
|
||||
const oldLegends = ensureArray(
|
||||
oldOptions?.legend || []
|
||||
) as LegendComponentOption[];
|
||||
const newLegends = ensureArray(
|
||||
newOptions?.legend || []
|
||||
) as LegendComponentOption[];
|
||||
return (
|
||||
oldLegends.some((l) => l.show && l.type === "custom") !==
|
||||
newLegends.some((l) => l.show && l.type === "custom")
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
@@ -312,7 +312,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
private _toValue = memoizeOne(
|
||||
(items: EntityNameItem[]): typeof this.value => {
|
||||
if (items.length === 0) {
|
||||
return "";
|
||||
return undefined;
|
||||
}
|
||||
if (items.length === 1) {
|
||||
const item = items[0];
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
getFloors,
|
||||
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
|
||||
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
|
||||
|
||||
export interface ClimateViewStrategyConfig {
|
||||
type: "climate";
|
||||
@@ -152,6 +153,7 @@ export class ClimateViewStrategy extends ReactiveElement {
|
||||
floorCount > 1
|
||||
? floor.name
|
||||
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
|
||||
icon: floor.icon || floorDefaultIcon(floor),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
generateEntityFilter,
|
||||
type EntityFilter,
|
||||
} from "../../../common/entity/entity_filter";
|
||||
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
@@ -98,6 +99,7 @@ export class LightViewStrategy extends ReactiveElement {
|
||||
floorCount > 1
|
||||
? floor.name
|
||||
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
|
||||
icon: floor.icon || floorDefaultIcon(floor),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -35,6 +35,8 @@ import { measureTextWidth } from "../../../../util/text";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { listenMediaQuery } from "../../../../common/dom/media_query";
|
||||
import { getEnergyColor } from "./common/color";
|
||||
import type { CustomLegendOption } from "../../../../components/chart/ha-chart-base";
|
||||
|
||||
@customElement("hui-energy-devices-graph-card")
|
||||
export class HuiEnergyDevicesGraphCard
|
||||
@@ -49,6 +51,8 @@ export class HuiEnergyDevicesGraphCard
|
||||
|
||||
@state() private _data?: EnergyData;
|
||||
|
||||
@state() private _legendData: NonNullable<CustomLegendOption["data"]> = [];
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "energy-devices-graph-chart-type",
|
||||
@@ -57,6 +61,14 @@ export class HuiEnergyDevicesGraphCard
|
||||
})
|
||||
private _chartType: "bar" | "pie" = "bar";
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "energy-devices-pie-hidden-stats",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _hiddenStats: string[] = [];
|
||||
|
||||
@state() private _isMobile = false;
|
||||
|
||||
private _compoundStats: string[] = [];
|
||||
@@ -121,10 +133,16 @@ export class HuiEnergyDevicesGraphCard
|
||||
<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.data=${this._chartData}
|
||||
.options=${this._createOptions(this._chartData, this._chartType)}
|
||||
.height=${`${Math.max(300, (this._chartData[0]?.data?.length || 0) * 28 + 50)}px`}
|
||||
@chart-click=${this._handleChartClick}
|
||||
.options=${this._createOptions(
|
||||
this._chartData,
|
||||
this._chartType,
|
||||
this._legendData
|
||||
)}
|
||||
.height=${`${Math.max(300, (this._legendData?.length || 0) * 28 + 50)}px`}
|
||||
.extraComponents=${[PieChart]}
|
||||
@chart-click=${this._handleChartClick}
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
></ha-chart-base>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -145,7 +163,8 @@ export class HuiEnergyDevicesGraphCard
|
||||
private _createOptions = memoizeOne(
|
||||
(
|
||||
data: (BarSeriesOption | PieSeriesOption)[],
|
||||
chartType: "bar" | "pie"
|
||||
chartType: "bar" | "pie",
|
||||
legendData: typeof this._legendData
|
||||
): ECOption => {
|
||||
const options: ECOption = {
|
||||
grid: {
|
||||
@@ -161,6 +180,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
},
|
||||
xAxis: { show: false },
|
||||
yAxis: { show: false },
|
||||
legend: { type: "custom", show: false },
|
||||
};
|
||||
if (chartType === "bar") {
|
||||
options.xAxis = {
|
||||
@@ -191,6 +211,18 @@ export class HuiEnergyDevicesGraphCard
|
||||
),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
options.legend = {
|
||||
type: "custom",
|
||||
show: true,
|
||||
data: legendData,
|
||||
selected: legendData
|
||||
.filter((d) => d.id && this._hiddenStats.includes(d.id))
|
||||
.reduce((acc, d) => {
|
||||
acc[d.id!] = false;
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
}
|
||||
return options;
|
||||
}
|
||||
@@ -354,23 +386,12 @@ 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;
|
||||
}
|
||||
|
||||
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 { summedData, compareSummedData } = getSummedData(energyData);
|
||||
const { consumption, compareConsumption } = computeConsumptionData(
|
||||
summedData,
|
||||
compareSummedData
|
||||
);
|
||||
const totalUsed = consumption.total.used_total;
|
||||
const showUntracked =
|
||||
"from_grid" in summedData ||
|
||||
@@ -380,6 +401,47 @@ export class HuiEnergyDevicesGraphCard
|
||||
? totalUsed -
|
||||
chartData.reduce((acc: number, d: any) => acc + d.value[0], 0)
|
||||
: 0;
|
||||
if (untracked > 0) {
|
||||
const color = getEnergyColor(
|
||||
computedStyle,
|
||||
this.hass.themes.darkMode,
|
||||
false,
|
||||
false,
|
||||
"--history-unknown-color"
|
||||
);
|
||||
chartData.push({
|
||||
id: "untracked",
|
||||
value: [untracked, "untracked"] as any,
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_devices_graph.untracked_consumption"
|
||||
),
|
||||
itemStyle: {
|
||||
color: color + "7F",
|
||||
borderColor: color,
|
||||
},
|
||||
});
|
||||
if (compareData) {
|
||||
const compareUntracked =
|
||||
compareConsumption!.total.used_total -
|
||||
chartDataCompare.reduce(
|
||||
(acc: number, d: any) => acc + d.value[0],
|
||||
0
|
||||
);
|
||||
if (compareUntracked > 0) {
|
||||
chartDataCompare.push({
|
||||
id: "untracked",
|
||||
value: [compareUntracked, "untracked"] as any,
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_devices_graph.untracked_consumption"
|
||||
),
|
||||
itemStyle: {
|
||||
color: color + "32",
|
||||
borderColor: color + "7F",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
datasets.push({
|
||||
type: "pie",
|
||||
radius: ["0%", compareData ? "30%" : "40%"],
|
||||
@@ -401,17 +463,36 @@ export class HuiEnergyDevicesGraphCard
|
||||
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) }
|
||||
)
|
||||
: "",
|
||||
show: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
chartData.sort((a: any, b: any) => b.value[0] - a.value[0]);
|
||||
if (
|
||||
this._config?.max_devices &&
|
||||
chartData.length > this._config.max_devices
|
||||
) {
|
||||
chartData.splice(this._config.max_devices);
|
||||
}
|
||||
|
||||
this._legendData = chartData.map((d) => ({
|
||||
...d,
|
||||
name: this._getDeviceName(d.name),
|
||||
}));
|
||||
// filter out hidden stats in place
|
||||
for (let i = chartData.length - 1; i >= 0; i--) {
|
||||
if (this._hiddenStats.includes((chartData[i] as any).id)) {
|
||||
chartData.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (compareData) {
|
||||
datasets[1].data = chartData.map((d) =>
|
||||
chartDataCompare.find((d2) => (d2 as any).id === d.id)
|
||||
) as typeof chartDataCompare;
|
||||
}
|
||||
|
||||
this._chartData = datasets;
|
||||
await this.updateComplete;
|
||||
}
|
||||
@@ -440,6 +521,18 @@ export class HuiEnergyDevicesGraphCard
|
||||
this._getStatistics(this._data!);
|
||||
}
|
||||
|
||||
private _datasetHidden(ev: CustomEvent<{ id: string }>) {
|
||||
this._hiddenStats = [...this._hiddenStats, ev.detail.id];
|
||||
this._getStatistics(this._data!);
|
||||
}
|
||||
|
||||
private _datasetUnhidden(ev: CustomEvent<{ id: string }>) {
|
||||
this._hiddenStats = this._hiddenStats.filter(
|
||||
(stat) => stat !== ev.detail.id
|
||||
);
|
||||
this._getStatistics(this._data!);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.card-header {
|
||||
display: flex;
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
DEFAULT_ENTITY_NAME,
|
||||
type EntityNameItem,
|
||||
} from "../../../../common/entity/compute_entity_name_display";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import type { EntityNameItem } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
/**
|
||||
* Computes the display name for an entity in Lovelace (cards and badges).
|
||||
*
|
||||
* @param hass - The Home Assistant instance
|
||||
* @param stateObj - The entity state object
|
||||
* @param nameConfig - The name configuration (string for override, or EntityNameItem[] for structured naming)
|
||||
* @param config - The name configuration (string for override, or EntityNameItem[] for structured naming)
|
||||
* @returns The computed entity name
|
||||
*/
|
||||
export const computeLovelaceEntityName = (
|
||||
hass: HomeAssistant,
|
||||
stateObj: HassEntity | undefined,
|
||||
nameConfig: string | EntityNameItem | EntityNameItem[] | undefined
|
||||
config: string | EntityNameItem | EntityNameItem[] | undefined
|
||||
): string => {
|
||||
if (typeof nameConfig === "string") {
|
||||
return nameConfig;
|
||||
// If no config is provided, fall back to the default state name
|
||||
if (!config) {
|
||||
return stateObj ? computeStateName(stateObj) : "";
|
||||
}
|
||||
if (typeof config === "string") {
|
||||
return config;
|
||||
}
|
||||
const config = nameConfig || DEFAULT_ENTITY_NAME;
|
||||
if (stateObj) {
|
||||
return hass.formatEntityName(stateObj, config);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { array, assert, assign, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
@@ -65,9 +64,7 @@ export class HuiAlarmPanelCardEditor
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {
|
||||
default_name: DEFAULT_ENTITY_NAME,
|
||||
},
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
|
||||
@@ -5,7 +5,6 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { assert, assign, boolean, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
@@ -73,7 +72,7 @@ export class HuiButtonCardEditor
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: { default_name: DEFAULT_ENTITY_NAME },
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
string,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
@@ -86,9 +85,7 @@ export class HuiEntityBadgeEditor
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {
|
||||
default_name: DEFAULT_ENTITY_NAME,
|
||||
},
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { assert, assign, boolean, object, optional, string } from "superstruct";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
import { headerFooterConfigStructs } from "../../header-footer/structs";
|
||||
@@ -26,9 +25,7 @@ const SCHEMA = [
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {
|
||||
default_name: DEFAULT_ENTITY_NAME,
|
||||
},
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
string,
|
||||
} from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import { NON_NUMERIC_ATTRIBUTES } from "../../../../data/entity_attributes";
|
||||
@@ -102,9 +101,7 @@ export class HuiGaugeCardEditor
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {
|
||||
default_name: DEFAULT_ENTITY_NAME,
|
||||
},
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from "superstruct";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
@@ -61,9 +60,7 @@ const SCHEMA = [
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {
|
||||
default_name: DEFAULT_ENTITY_NAME,
|
||||
},
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { mdiGestureTap } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { assert, assign, object, optional, string } from "superstruct";
|
||||
import { mdiGestureTap } from "@mdi/js";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -37,9 +36,7 @@ const SCHEMA = [
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {
|
||||
default_name: DEFAULT_ENTITY_NAME,
|
||||
},
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { assert, assign, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
@@ -33,9 +32,7 @@ const SCHEMA = [
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {
|
||||
default_name: DEFAULT_ENTITY_NAME,
|
||||
},
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { mdiGestureTap } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
assert,
|
||||
assign,
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
} from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
@@ -71,9 +70,7 @@ export class HuiPictureEntityCardEditor
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {
|
||||
default_name: DEFAULT_ENTITY_NAME,
|
||||
},
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { assert, assign, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -25,9 +24,7 @@ const SCHEMA = [
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {
|
||||
default_name: DEFAULT_ENTITY_NAME,
|
||||
},
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
assert,
|
||||
assign,
|
||||
@@ -12,18 +12,17 @@ import {
|
||||
string,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-sensor-card";
|
||||
import type { SensorCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { entityNameStruct } from "../structs/entity-name-struct";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-sensor-card";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
@@ -71,9 +70,7 @@ export class HuiSensorCardEditor
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {
|
||||
default_name: DEFAULT_ENTITY_NAME,
|
||||
},
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "superstruct";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
@@ -35,7 +35,6 @@ import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import "./hui-card-features-editor";
|
||||
import type { FeatureType } from "./hui-card-features-editor";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
|
||||
const COMPATIBLE_FEATURES_TYPES: Record<string, FeatureType[]> = {
|
||||
climate: [
|
||||
@@ -89,9 +88,7 @@ export class HuiThermostatCardEditor
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {
|
||||
default_name: DEFAULT_ENTITY_NAME,
|
||||
},
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
} from "superstruct";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { orderProperties } from "../../../../common/util/order-properties";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
@@ -102,9 +101,7 @@ export class HuiTileCardEditor
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {
|
||||
default_name: DEFAULT_ENTITY_NAME,
|
||||
},
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
string,
|
||||
} from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
@@ -153,9 +152,7 @@ export class HuiWeatherForecastCardEditor
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {
|
||||
default_name: DEFAULT_ENTITY_NAME,
|
||||
},
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
@@ -94,9 +93,7 @@ export class HuiHeadingEntityEditor
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {
|
||||
default_name: DEFAULT_ENTITY_NAME,
|
||||
},
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
|
||||
@@ -168,9 +168,17 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
|
||||
const summaryEntities = Object.values(entitiesBySummary).flat();
|
||||
|
||||
// Automations section
|
||||
const automationFilter = generateEntityFilter(hass, {
|
||||
domain: "automation",
|
||||
entity_category: "none",
|
||||
});
|
||||
const automations = areaEntities.filter(automationFilter);
|
||||
|
||||
// Rest of entities grouped by device
|
||||
const otherEntities = areaEntities.filter(
|
||||
(entityId) => !summaryEntities.includes(entityId)
|
||||
(entityId) =>
|
||||
!summaryEntities.includes(entityId) && !automations.includes(entityId)
|
||||
);
|
||||
|
||||
const entitiesByDevice: Record<string, string[]> = {};
|
||||
@@ -295,6 +303,21 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
sections.push(...deviceSections);
|
||||
}
|
||||
|
||||
// Show automations last, if they exist
|
||||
if (automations.length > 0) {
|
||||
sections.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
computeHeadingCard(
|
||||
hass.localize("ui.panel.lovelace.strategy.home.automations"),
|
||||
"mdi:robot",
|
||||
"/config/automation/dashboard"
|
||||
),
|
||||
...automations.map(computeTileCard),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
|
||||
const maxColumns = clamp(sections.length, 2, 3);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
|
||||
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
|
||||
import { getHomeStructure } from "./helpers/home-structure";
|
||||
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
|
||||
|
||||
export interface HomeMainViewStrategyConfig {
|
||||
type: "home-main";
|
||||
@@ -92,7 +93,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
||||
? floor.name
|
||||
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
|
||||
heading_style: "title",
|
||||
icon: floor.icon,
|
||||
icon: floor.icon || floorDefaultIcon(floor),
|
||||
},
|
||||
...cards,
|
||||
],
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
findEntities,
|
||||
generateEntityFilter,
|
||||
} from "../../../../common/entity/entity_filter";
|
||||
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
@@ -96,6 +97,7 @@ export class HomeMMediaPlayersViewStrategy extends ReactiveElement {
|
||||
floorCount > 1
|
||||
? floor.name
|
||||
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
|
||||
icon: floor.icon || floorDefaultIcon(floor),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
generateEntityFilter,
|
||||
type EntityFilter,
|
||||
} from "../../../common/entity/entity_filter";
|
||||
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
@@ -140,6 +141,7 @@ export class SafetyViewStrategy extends ReactiveElement {
|
||||
floorCount > 1
|
||||
? floor.name
|
||||
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
|
||||
icon: floor.icon || floorDefaultIcon(floor),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -255,6 +255,10 @@ class DialogTodoItemEditor extends LitElement {
|
||||
|
||||
// Parse a date in the browser timezone
|
||||
private _parseDate(dateStr: string): Date {
|
||||
// If it's a date-only string (no 'T'), parse as midnight in browser time to avoid offset issues
|
||||
if (!dateStr.includes("T")) {
|
||||
return new Date(dateStr + "T00:00:00");
|
||||
}
|
||||
const tzDate = new TZDate(dateStr, this._timeZone!);
|
||||
return new Date(tzDate.getTime());
|
||||
}
|
||||
|
||||
@@ -6943,7 +6943,8 @@
|
||||
"areas": "Areas",
|
||||
"other_areas": "Other areas",
|
||||
"unamed_device": "Unnamed device",
|
||||
"others": "Others"
|
||||
"others": "Others",
|
||||
"automations": "Automations"
|
||||
},
|
||||
"common_controls": {
|
||||
"not_loaded": "Usage Prediction integration is not loaded.",
|
||||
@@ -7075,10 +7076,10 @@
|
||||
"energy_devices_graph": {
|
||||
"energy_usage": "Energy usage",
|
||||
"previous_energy_usage": "Previous energy usage",
|
||||
"total_energy_usage": "Total energy usage",
|
||||
"total_energy_usage": "Total",
|
||||
"change_chart_type": "Change chart type",
|
||||
"untracked": "untracked",
|
||||
"includes_untracked": "Includes {num} kWh of untracked energy"
|
||||
"untracked_consumption": "Untracked consumption",
|
||||
"untracked": "untracked"
|
||||
},
|
||||
"energy_devices_detail_graph": {
|
||||
"untracked_consumption": "Untracked consumption",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../../../../src/common/entity/compute_entity_name_display";
|
||||
import { computeLovelaceEntityName } from "../../../../../src/panels/lovelace/common/entity/compute-lovelace-entity-name";
|
||||
import type { HomeAssistant } from "../../../../../src/types";
|
||||
import { mockStateObj } from "../../../../common/entity/context/context-mock";
|
||||
@@ -23,30 +22,32 @@ describe("computeLovelaceEntityName", () => {
|
||||
expect(mockFormatEntityName).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns empty string when nameConfig is empty string", () => {
|
||||
it("return state name when nameConfig is empty string", () => {
|
||||
const mockFormatEntityName = vi.fn();
|
||||
const hass = createMockHass(mockFormatEntityName);
|
||||
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
|
||||
const stateObj = mockStateObj({
|
||||
entity_id: "light.kitchen",
|
||||
attributes: { friendly_name: "Kitchen Light" },
|
||||
});
|
||||
|
||||
const result = computeLovelaceEntityName(hass, stateObj, "");
|
||||
|
||||
expect(result).toBe("");
|
||||
expect(result).toBe("Kitchen Light");
|
||||
expect(mockFormatEntityName).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls formatEntityName with DEFAULT_ENTITY_NAME when nameConfig is undefined", () => {
|
||||
it("return state name when nameConfig is undefined", () => {
|
||||
const mockFormatEntityName = vi.fn(() => "Formatted Name");
|
||||
const hass = createMockHass(mockFormatEntityName);
|
||||
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
|
||||
const stateObj = mockStateObj({
|
||||
entity_id: "light.kitchen",
|
||||
attributes: { friendly_name: "Kitchen Light" },
|
||||
});
|
||||
|
||||
const result = computeLovelaceEntityName(hass, stateObj, undefined);
|
||||
|
||||
expect(result).toBe("Formatted Name");
|
||||
expect(mockFormatEntityName).toHaveBeenCalledTimes(1);
|
||||
expect(mockFormatEntityName).toHaveBeenCalledWith(
|
||||
stateObj,
|
||||
DEFAULT_ENTITY_NAME
|
||||
);
|
||||
expect(result).toBe("Kitchen Light");
|
||||
expect(mockFormatEntityName).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls formatEntityName with EntityNameItem config", () => {
|
||||
|
||||
Reference in New Issue
Block a user