Compare commits

...

10 Commits

Author SHA1 Message Date
Bram Kragten
1770a51303 Bumped version to 20250327.0 2025-03-27 16:46:17 +01:00
Paul Bottein
534df3d378 Add loading state to area strategy (#24803) 2025-03-27 16:44:15 +01:00
Paul Bottein
23229b3e3b Set the max number of columns to 3 for area dashboard (#24802)
* Set the max number of columns to 4 for area dashboard

* Set it to 3
2025-03-27 16:44:14 +01:00
karwosts
94ee99160b Energy device settings fixes (#24801) 2025-03-27 16:44:13 +01:00
Paul Bottein
b009d71e8f Fix take control of the dashboard (#24800) 2025-03-27 16:44:12 +01:00
Bram Kragten
2ab8209622 Align behavior of template selector with text selector (#24796) 2025-03-27 16:44:11 +01:00
Paul Bottein
ed2940edc3 Revert "Restore scroll position when using back navigation in dashboard" (#24795)
Revert "Restore scroll position when using back navigation in dashboard (#24777)"

This reverts commit 9cfcd21a93dd50d61fb64039ce7bec973c721806.
2025-03-27 16:44:11 +01:00
Paul Bottein
e2b9a06242 Fix more info for disabled entities (#24789) 2025-03-27 16:44:10 +01:00
Paul Bottein
a7acee0438 Remove fixed height in ha tile info (#24787)
Remove unused height in ha tile info
2025-03-27 16:44:09 +01:00
Bram Kragten
1208af510c Fix typo in Arithmetic (#24786)
Fix type in Arithmetic
2025-03-27 16:44:08 +01:00
18 changed files with 173 additions and 107 deletions

View File

@ -309,7 +309,7 @@ export class HcMain extends HassElement {
"../../../../src/panels/lovelace/strategies/get-strategy"
);
const config = await generateLovelaceDashboardStrategy(
rawConfig.strategy,
rawConfig,
this.hass!
);
this._handleNewLovelaceConfig(config);
@ -351,10 +351,7 @@ export class HcMain extends HassElement {
"../../../../src/panels/lovelace/strategies/get-strategy"
);
this._handleNewLovelaceConfig(
await generateLovelaceDashboardStrategy(
DEFAULT_CONFIG.strategy,
this.hass!
)
await generateLovelaceDashboardStrategy(DEFAULT_CONFIG, this.hass!)
);
}

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20250326.0"
version = "20250327.0"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"

View File

@ -1,7 +1,11 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { AreaRegistryEntry } from "../../data/area_registry";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
ExtEntityRegistryEntry,
} from "../../data/entity_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import type { HomeAssistant } from "../../types";
@ -19,6 +23,23 @@ export const getEntityContext = (
| EntityRegistryDisplayEntry
| undefined;
if (!entry) {
return {
device: null,
area: null,
floor: null,
};
}
return getEntityEntryContext(entry, hass);
};
export const getEntityEntryContext = (
entry:
| EntityRegistryDisplayEntry
| EntityRegistryEntry
| ExtEntityRegistryEntry,
hass: HomeAssistant
): EntityContext => {
const deviceId = entry?.device_id;
const device = deviceId ? hass.devices[deviceId] : null;
const areaId = entry?.area_id || device?.area_id;

View File

@ -69,11 +69,14 @@ export class HaTemplateSelector extends LitElement {
}
private _handleChange(ev) {
const value = ev.target.value;
let value = ev.target.value;
if (this.value === value) {
return;
}
this.warn = WARNING_STRINGS.find((str) => value.includes(str));
if (value === "" && !this.required) {
value = undefined;
}
fireEvent(this, "value-changed", { value });
}

View File

@ -26,7 +26,6 @@ export class HaTileInfo extends LitElement {
flex-direction: column;
align-items: flex-start;
justify-content: center;
height: 36px;
}
span {
text-overflow: ellipsis;

View File

@ -38,7 +38,7 @@ export interface Statistic {
export enum StatisticMeanType {
NONE = 0,
ARIMETHIC = 1,
ARITHMETIC = 1,
CIRCULAR = 2,
}

View File

@ -21,8 +21,10 @@ import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { getEntityContext } from "../../common/entity/get_entity_context";
import {
computeEntityEntryName,
computeEntityName,
} from "../../common/entity/compute_entity_name";
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
import { navigate } from "../../common/navigate";
import "../../components/ha-button-menu";
@ -56,6 +58,10 @@ import "./ha-more-info-history-and-logbook";
import "./ha-more-info-info";
import "./ha-more-info-settings";
import "./more-info-content";
import {
getEntityContext,
getEntityEntryContext,
} from "../../common/entity/get_entity_context";
export interface MoreInfoDialogParams {
entityId: string | null;
@ -293,11 +299,18 @@ export class MoreInfoDialog extends LitElement {
this._initialView !== DEFAULT_VIEW && !this._childView;
const showCloseIcon = isDefaultView || isSpecificInitialView;
const context = stateObj ? getEntityContext(stateObj, this.hass) : null;
const context = stateObj
? getEntityContext(stateObj, this.hass)
: this._entry
? getEntityEntryContext(this._entry, this.hass)
: undefined;
const entityName = stateObj
? computeEntityName(stateObj, this.hass)
: undefined;
: this._entry
? computeEntityEntryName(this._entry, this.hass)
: entityId;
const deviceName = context?.device
? computeDeviceName(context.device)
: undefined;

View File

@ -152,12 +152,14 @@ export class EnergyDeviceSettings extends LitElement {
device_consumptions: this.preferences
.device_consumption as DeviceConsumptionEnergyPreference[],
saveCallback: async (newDevice) => {
await this._savePreferences({
const newPrefs = {
...this.preferences,
device_consumption: this.preferences.device_consumption.map((d) =>
d === origDevice ? newDevice : d
),
});
};
this._sanitizeParents(newPrefs);
await this._savePreferences(newPrefs);
},
});
}
@ -177,6 +179,15 @@ export class EnergyDeviceSettings extends LitElement {
});
}
private _sanitizeParents(prefs: EnergyPreferences) {
const statIds = prefs.device_consumption.map((d) => d.stat_consumption);
prefs.device_consumption.forEach((d) => {
if (d.included_in_stat && !statIds.includes(d.included_in_stat)) {
delete d.included_in_stat;
}
});
}
private async _deleteDevice(ev) {
const deviceToDelete: DeviceConsumptionEnergyPreference =
ev.currentTarget.device;
@ -196,14 +207,7 @@ export class EnergyDeviceSettings extends LitElement {
(device) => device !== deviceToDelete
),
};
newPrefs.device_consumption.forEach((d, idx) => {
if (d.included_in_stat === deviceToDelete.stat_consumption) {
newPrefs.device_consumption[idx] = {
...newPrefs.device_consumption[idx],
};
delete newPrefs.device_consumption[idx].included_in_stat;
}
});
this._sanitizeParents(newPrefs);
await this._savePreferences(newPrefs);
} catch (err: any) {
showAlertDialog(this, { title: `Failed to save config: ${err.message}` });

View File

@ -74,6 +74,7 @@ export class DialogEnergyDeviceSettings
this._possibleParents = this._params.device_consumptions.filter(
(d) =>
d.stat_consumption !== this._device!.stat_consumption &&
d.stat_consumption !== this._params?.device?.stat_consumption &&
!children.includes(d.stat_consumption)
);
}
@ -160,18 +161,26 @@ export class DialogEnergyDeviceSettings
naturalMenuWidth
clearable
>
${this._possibleParents.map(
(stat) => html`
<mwc-list-item .value=${stat.stat_consumption}
>${stat.name ||
getStatisticLabel(
this.hass,
stat.stat_consumption,
this._params?.statsMetadata?.[stat.stat_consumption]
)}</mwc-list-item
>
`
)}
${!this._possibleParents.length
? html`
<mwc-list-item disabled value="-"
>${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.no_upstream_devices"
)}</mwc-list-item
>
`
: this._possibleParents.map(
(stat) => html`
<mwc-list-item .value=${stat.stat_consumption}
>${stat.name ||
getStatisticLabel(
this.hass,
stat.stat_consumption,
this._params?.statsMetadata?.[stat.stat_consumption]
)}</mwc-list-item
>
`
)}
</ha-select>
<mwc-button @click=${this.closeDialog} slot="secondaryAction">

View File

@ -187,7 +187,7 @@ export class LovelacePanel extends LitElement {
private async _regenerateConfig() {
const conf = await generateLovelaceDashboardStrategy(
DEFAULT_CONFIG.strategy,
DEFAULT_CONFIG,
this.hass!
);
this._setLovelaceConfig(conf, DEFAULT_CONFIG, "generated");
@ -281,10 +281,7 @@ export class LovelacePanel extends LitElement {
// We need these to generate a dashboard, wait for them
return;
}
conf = await generateLovelaceDashboardStrategy(
rawConf.strategy,
this.hass!
);
conf = await generateLovelaceDashboardStrategy(rawConf, this.hass!);
} else {
conf = rawConf;
}
@ -301,7 +298,7 @@ export class LovelacePanel extends LitElement {
return;
}
conf = await generateLovelaceDashboardStrategy(
DEFAULT_CONFIG.strategy,
DEFAULT_CONFIG,
this.hass!
);
rawConf = DEFAULT_CONFIG;
@ -378,10 +375,7 @@ export class LovelacePanel extends LitElement {
let conf: LovelaceConfig;
// If strategy defined, apply it here.
if (isStrategyDashboard(newConfig)) {
conf = await generateLovelaceDashboardStrategy(
newConfig.strategy,
this.hass!
);
conf = await generateLovelaceDashboardStrategy(newConfig, this.hass!);
} else {
conf = newConfig;
}
@ -415,7 +409,7 @@ export class LovelacePanel extends LitElement {
try {
// Optimistic update
const generatedConf = await generateLovelaceDashboardStrategy(
DEFAULT_CONFIG.strategy,
DEFAULT_CONFIG,
this.hass!
);
this._updateLovelace({

View File

@ -76,9 +76,9 @@ import { getLovelaceStrategy } from "./strategies/get-strategy";
import { isLegacyStrategyConfig } from "./strategies/legacy-strategy";
import type { Lovelace } from "./types";
import "./views/hui-view";
import "./views/hui-view-container";
import type { HUIView } from "./views/hui-view";
import "./views/hui-view-background";
import "./views/hui-view-container";
@customElement("hui-root")
class HUIRoot extends LitElement {
@ -99,8 +99,6 @@ class HUIRoot extends LitElement {
private _viewCache?: Record<string, HUIView>;
private _viewScrollPositions: Record<string, number> = {};
private _debouncedConfigChanged: () => void;
private _conversation = memoizeOne((_components) =>
@ -112,7 +110,7 @@ class HUIRoot extends LitElement {
// The view can trigger a re-render when it knows that certain
// web components have been loaded.
this._debouncedConfigChanged = debounce(
() => this._selectView(this._curView, true, false),
() => this._selectView(this._curView, true),
100,
false
);
@ -527,22 +525,13 @@ class HUIRoot extends LitElement {
window.addEventListener("scroll", this._handleWindowScroll, {
passive: true,
});
window.addEventListener("popstate", this._handlePopState);
}
public disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener("scroll", this._handleWindowScroll);
window.removeEventListener("popstate", this._handlePopState);
}
private _restoreScroll = false;
private _handlePopState = () => {
// If we navigated back, we want to restore the scroll position.
this._restoreScroll = true;
};
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
@ -583,6 +572,9 @@ class HUIRoot extends LitElement {
}
newSelectView = index;
}
// Will allow to override history scroll restoration when using back button
setTimeout(() => scrollTo({ behavior: "auto", top: 0 }), 1);
}
if (changedProperties.has("lovelace")) {
@ -621,10 +613,7 @@ class HUIRoot extends LitElement {
newSelectView = this._curView;
}
// Will allow for ripples to start rendering
afterNextRender(() => {
this._selectView(newSelectView, force, this._restoreScroll);
this._restoreScroll = false;
});
afterNextRender(() => this._selectView(newSelectView, force));
}
}
@ -932,26 +921,17 @@ class HUIRoot extends LitElement {
}
}
private _selectView(
viewIndex: HUIRoot["_curView"],
force: boolean,
restoreScroll: boolean
): void {
private _selectView(viewIndex: HUIRoot["_curView"], force: boolean): void {
if (!force && this._curView === viewIndex) {
return;
}
if (this._curView != null) {
this._viewScrollPositions[this._curView] = window.scrollY;
}
viewIndex = viewIndex === undefined ? 0 : viewIndex;
this._curView = viewIndex;
if (force) {
this._viewCache = {};
this._viewScrollPositions = {};
}
// Recreate a new element to clear the applied themes.
@ -983,15 +963,10 @@ class HUIRoot extends LitElement {
if (!force && this._viewCache![viewIndex]) {
view = this._viewCache![viewIndex];
const position = restoreScroll
? this._viewScrollPositions[viewIndex] || 0
: 0;
setTimeout(() => scrollTo({ behavior: "auto", top: position }), 0);
} else {
view = document.createElement("hui-view");
view.index = viewIndex;
this._viewCache![viewIndex] = view;
setTimeout(() => scrollTo({ behavior: "auto", top: 0 }), 0);
}
view.lovelace = this.lovelace;

View File

@ -185,7 +185,7 @@ export class HuiSection extends ReactiveElement {
if (isStrategySection(sectionConfig)) {
isStrategy = true;
sectionConfig = await generateLovelaceSectionStrategy(
sectionConfig.strategy,
sectionConfig,
this.hass!
);
}

View File

@ -1,5 +1,6 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { clamp } from "../../../../common/number/clamp";
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
@ -144,7 +145,10 @@ export class AreaViewStrategy extends ReactiveElement {
});
}
// Take the full width if there is only one section to avoid misalignment between cards and header
// 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);
// Take the full width if there is only one section to avoid narrow header on desktop
if (sections.length === 1) {
sections[0].column_span = 2;
}
@ -160,7 +164,7 @@ export class AreaViewStrategy extends ReactiveElement {
content: `## ${area.name}`,
},
},
max_columns: 2,
max_columns: maxColumns,
sections: sections,
badges: badges,
};

View File

@ -1,13 +1,14 @@
import { STATE_NOT_RUNNING } from "home-assistant-js-websocket";
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type { LovelaceStrategyEditor } from "../types";
import type {
AreaViewStrategyConfig,
EntitiesDisplay,
} from "./area-view-strategy";
import type { LovelaceStrategyEditor } from "../types";
import type { AreasViewStrategyConfig } from "./areas-overview-view-strategy";
import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helper";
@ -30,6 +31,28 @@ export class AreasDashboardStrategy extends ReactiveElement {
config: AreasDashboardStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceConfig> {
if (hass.config.state === STATE_NOT_RUNNING) {
return {
views: [
{
type: "sections",
sections: [{ cards: [{ type: "starting" }] }],
},
],
};
}
if (hass.config.recovery_mode) {
return {
views: [
{
type: "sections",
sections: [{ cards: [{ type: "recovery-mode" }] }],
},
],
};
}
const areas = getAreas(
hass.areas,
config.areas_display?.hidden,

View File

@ -95,7 +95,7 @@ export class AreasOverviewViewStrategy extends ReactiveElement {
return {
type: "sections",
max_columns: 2,
max_columns: 3,
sections: areaSections,
};
}

View File

@ -1,10 +1,18 @@
import type {
LovelaceSectionConfig,
LovelaceStrategySectionConfig,
} from "../../../data/lovelace/config/section";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type {
LovelaceConfig,
LovelaceDashboardStrategyConfig,
LovelaceRawConfig,
} from "../../../data/lovelace/config/types";
import { isStrategyDashboard } from "../../../data/lovelace/config/types";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type {
LovelaceStrategyViewConfig,
LovelaceViewConfig,
} from "../../../data/lovelace/config/view";
import { isStrategyView } from "../../../data/lovelace/config/view";
import type { AsyncReturnType, HomeAssistant } from "../../../types";
import { cleanLegacyStrategyConfig, isLegacyStrategy } from "./legacy-strategy";
@ -133,10 +141,11 @@ const generateStrategy = async <T extends LovelaceStrategyConfigType>(
};
export const generateLovelaceDashboardStrategy = async (
strategyConfig: LovelaceStrategyConfig,
config: LovelaceDashboardStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceConfig> =>
generateStrategy(
): Promise<LovelaceConfig> => {
const { strategy, ...base } = config;
const generated = generateStrategy(
"dashboard",
(err) => ({
views: [
@ -151,15 +160,21 @@ export const generateLovelaceDashboardStrategy = async (
},
],
}),
strategyConfig,
strategy,
hass
);
return {
...base,
...generated,
};
};
export const generateLovelaceViewStrategy = async (
strategyConfig: LovelaceStrategyConfig,
config: LovelaceStrategyViewConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> =>
generateStrategy(
): Promise<LovelaceViewConfig> => {
const { strategy, ...base } = config;
const generated = await generateStrategy(
"view",
(err) => ({
cards: [
@ -169,15 +184,21 @@ export const generateLovelaceViewStrategy = async (
},
],
}),
strategyConfig,
strategy,
hass
);
return {
...base,
...generated,
};
};
export const generateLovelaceSectionStrategy = async (
strategyConfig: LovelaceStrategyConfig,
config: LovelaceStrategySectionConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> =>
generateStrategy(
): Promise<LovelaceSectionConfig> => {
const { strategy, ...base } = config;
const generated = await generateStrategy(
"section",
(err) => ({
cards: [
@ -187,9 +208,14 @@ export const generateLovelaceSectionStrategy = async (
},
],
}),
strategyConfig,
strategy,
hass
);
return {
...base,
...generated,
};
};
/**
* Find all references to strategies and replaces them with the generated output
@ -199,20 +225,20 @@ export const expandLovelaceConfigStrategies = async (
hass: HomeAssistant
): Promise<LovelaceConfig> => {
const newConfig = isStrategyDashboard(config)
? await generateLovelaceDashboardStrategy(config.strategy, hass)
? await generateLovelaceDashboardStrategy(config, hass)
: { ...config };
newConfig.views = await Promise.all(
newConfig.views.map(async (view) => {
const newView = isStrategyView(view)
? await generateLovelaceViewStrategy(view.strategy, hass)
? await generateLovelaceViewStrategy(view, hass)
: { ...view };
if (newView.sections) {
newView.sections = await Promise.all(
newView.sections.map(async (section) => {
const newSection = isStrategyView(section)
? await generateLovelaceSectionStrategy(section.strategy, hass)
? await generateLovelaceSectionStrategy(section, hass)
: { ...section };
return newSection;
})

View File

@ -233,10 +233,7 @@ export class HUIView extends ReactiveElement {
if (isStrategyView(viewConfig)) {
isStrategy = true;
viewConfig = await generateLovelaceViewStrategy(
viewConfig.strategy,
this.hass!
);
viewConfig = await generateLovelaceViewStrategy(viewConfig, this.hass!);
}
viewConfig = {

View File

@ -2902,7 +2902,8 @@
"device_consumption_energy": "Device energy consumption",
"selected_stat_intro": "Select the energy sensor that measures the device's energy usage in either of {unit}.",
"included_in_device": "Upstream device",
"included_in_device_helper": "If this device is already counted by another device (such as a smart switch measured by a smart breaker), selecting the upstream device prevents duplicate energy tracking."
"included_in_device_helper": "If this device is already counted by another device (such as a smart switch measured by a smart breaker), selecting the upstream device prevents duplicate energy tracking.",
"no_upstream_devices": "No eligible upstream devices"
}
}
},
@ -8129,7 +8130,7 @@
},
"mean_type": {
"0": "None",
"1": "Arimethic",
"1": "Arithmetic",
"2": "Circular"
},
"fix_issue": {