Compare commits

...

21 Commits

Author SHA1 Message Date
renovate[bot]
9c9d274b5c Update dependency typescript-eslint to v8.48.0 (#28196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-27 19:20:07 +01:00
Paul Bottein
d6e6bc0e80 Fix safe area for sidebar section views in Android (#28194) 2025-11-27 19:46:56 +02:00
ildar170975
530a70b168 Automations, scripts, scenes: add a tooltip for relative time (#28158)
* add a tooltip for last_triggered

* add a tooltip for last_triggered

* add a tooltip for last_activated

* Apply suggestions from code review

* Apply suggestion from @MindFreeze

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Apply suggestion from @MindFreeze

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Apply suggestion from @MindFreeze

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-27 19:44:11 +02:00
Wendelin
23137500f8 Add TCA by target sort like item collections (#28192) 2025-11-27 17:03:27 +01:00
Wendelin
63e7ed21a4 Fix lab automations icons and sidebar width (#28184)
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-27 16:57:42 +01:00
Petar Petrov
92611f46f4 Fix water sankey calculation to include total supply from sources (#28191) 2025-11-27 16:44:56 +01:00
Bram Kragten
9bc896241d Always store token when using develop and serve (#28179) 2025-11-27 16:43:36 +01:00
Paul Bottein
2baafe620c Add hint to reorder areas and floors (#28189) 2025-11-27 16:32:23 +01:00
Wendelin
ce52bbaf8c Show hidden entities in target tree (#28181)
* Show hidden entities in target tree

* Fix types
2025-11-27 15:44:50 +02:00
Wendelin
0b4b8d9082 "Add TCA" dialog desktop height to 800px (#28182) 2025-11-27 14:42:18 +01:00
Petar Petrov
bddbb773b7 Fix sankey chart resizing (#28180) 2025-11-27 15:41:46 +02:00
Paul Bottein
d52e1e8835 Use hui-root for panel energy (#28149)
* Use hui-root for panel energy

* Review feedback

* Set empty prefs
2025-11-27 15:35:36 +02:00
Petar Petrov
0a9dccfd19 Refactor power sankey hierarchy to handle devices with not power sensor (#28164) 2025-11-27 12:21:55 +01:00
Petar Petrov
bfd78670cc Disable axis pointer on the energy devices bar chart to fix refresh issues on touch devices (#28163) 2025-11-27 12:20:06 +01:00
Petar Petrov
11276af1a0 Handle grouping by floor and area in power sankey card (#28162) 2025-11-27 12:19:42 +01:00
Paul Bottein
d7be46c00b Fix box shadow for sidebar tabs (#28170) 2025-11-27 12:19:15 +01:00
Paul Bottein
94f32ce242 Fix disabled dashboard picker when no custom dashboard (#28172) 2025-11-27 12:18:48 +01:00
Wendelin
ef3e8186bc Fix add condition default tab and blank styles (#28166) 2025-11-27 12:18:21 +01:00
Paul Bottein
50fcf622aa Fix labs back button (#28174) 2025-11-27 12:42:55 +02:00
Paul Bottein
77c2444be8 Restore sidebar view when clicking back (#28167) 2025-11-27 12:42:36 +02:00
Wendelin
e5cb26cd3d Fix automation add TCA autofocus (#28168)
Fix automation add tca autofocus
2025-11-27 11:28:18 +01:00
33 changed files with 625 additions and 437 deletions

View File

@@ -217,7 +217,7 @@
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.47.0",
"typescript-eslint": "8.48.0",
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.13",
"webpack-stats-plugin": "1.1.3",

View File

@@ -1,5 +1,6 @@
import type { AuthData } from "home-assistant-js-websocket";
import { extractSearchParam } from "../url/search-params";
import { hassUrl } from "../../data/auth";
declare global {
interface Window {
@@ -30,7 +31,11 @@ export function askWrite() {
export function saveTokens(tokens: AuthData | null) {
tokenCache.tokens = tokens;
if (!tokenCache.writeEnabled && extractSearchParam("storeToken") === "true") {
if (
!tokenCache.writeEnabled &&
(extractSearchParam("storeToken") === "true" ||
hassUrl !== `${location.protocol}//${location.host}`)
) {
tokenCache.writeEnabled = true;
}

View File

@@ -279,6 +279,7 @@ export class HaSankeyChart extends LitElement {
:host {
display: block;
flex: 1;
max-width: 100%;
background: var(--ha-card-background, var(--card-background-color));
}
ha-chart-base {

View File

@@ -202,6 +202,7 @@ export class HaControlSelect extends LitElement {
color: var(--primary-text-color);
user-select: none;
-webkit-tap-highlight-color: transparent;
border-radius: var(--control-select-border-radius);
}
:host([vertical]) {
width: var(--control-select-thickness);
@@ -211,7 +212,6 @@ export class HaControlSelect extends LitElement {
position: relative;
height: 100%;
width: 100%;
border-radius: var(--control-select-border-radius);
transform: translateZ(0);
display: flex;
flex-direction: row;

View File

@@ -186,6 +186,7 @@ export class HaIcon extends LitElement {
static styles = css`
:host {
display: flex;
fill: currentcolor;
}
`;

View File

@@ -1,3 +1,5 @@
import "@home-assistant/webawesome/dist/components/dialog/dialog";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement } from "lit";
import {
customElement,
@@ -7,8 +9,6 @@ import {
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { mdiClose } from "@mdi/js";
import "@home-assistant/webawesome/dist/components/dialog/dialog";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
@@ -172,7 +172,9 @@ export class HaWaDialog extends LitElement {
await this.updateComplete;
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
requestAnimationFrame(() => {
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
});
};
private _handleAfterShow = () => {

View File

@@ -75,17 +75,11 @@ export const reorderAreaRegistryEntries = (
});
export const getAreaEntityLookup = (
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[],
filterHidden = false
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[]
): AreaEntityLookup => {
const areaEntityLookup: AreaEntityLookup = {};
for (const entity of entities) {
if (
!entity.area_id ||
(filterHidden &&
((entity as EntityRegistryDisplayEntry).hidden ||
(entity as EntityRegistryEntry).hidden_by))
) {
if (!entity.area_id) {
continue;
}
if (!(entity.area_id in areaEntityLookup)) {

View File

@@ -111,17 +111,11 @@ export const sortDeviceRegistryByName = (
);
export const getDeviceEntityLookup = (
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[],
filterHidden = false
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[]
): DeviceEntityLookup => {
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (
!entity.device_id ||
(filterHidden &&
((entity as EntityRegistryDisplayEntry).hidden ||
(entity as EntityRegistryEntry).hidden_by))
) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {

View File

@@ -84,6 +84,8 @@ export class HaConfigAreasDashboard extends LitElement {
@property({ attribute: false }) public route!: Route;
private _searchParms = new URLSearchParams(window.location.search);
@state() private _hierarchy?: AreasFloorHierarchy;
private _blockHierarchyUpdate = false;
@@ -167,7 +169,9 @@ export class HaConfigAreasDashboard extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
back-path="/config"
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.tabs=${configSections.areas}
.route=${this.route}
has-fab

View File

@@ -294,10 +294,7 @@ class DialogAddAutomationElement
feature.domain === "automation" &&
feature.preview_feature === "new_triggers_conditions"
)?.enabled ?? false;
this._tab =
this._newTriggersAndConditions && this._params?.type !== "condition"
? "targets"
: "groups";
this._tab = this._newTriggersAndConditions ? "targets" : "groups";
}
);
@@ -1353,6 +1350,61 @@ class DialogAddAutomationElement
this._labelRegistry?.find(({ label_id }) => label_id === labelId)
);
private _getDomainType(domain: string) {
return ENTITY_DOMAINS_MAIN.has(domain) ||
(this._manifests?.[domain].integration_type === "entity" &&
!ENTITY_DOMAINS_OTHER.has(domain))
? "dynamicGroups"
: this._manifests?.[domain].integration_type === "helper"
? "helpers"
: "other";
}
private _sortDomainsByCollection(
type: AddAutomationElementDialogParams["type"],
entries: [
string,
{ title: string; items: AddAutomationElementListItem[] },
][]
): { title: string; items: AddAutomationElementListItem[] }[] {
const order: string[] = [];
TYPES[type].collections.forEach((collection) => {
order.push(...Object.keys(collection.groups));
});
return entries
.sort((a, b) => {
const domainA = a[0];
const domainB = b[0];
if (order.includes(domainA) && order.includes(domainB)) {
return order.indexOf(domainA) - order.indexOf(domainB);
}
let typeA = domainA;
let typeB = domainB;
if (!order.includes(domainA)) {
typeA = this._getDomainType(domainA);
}
if (!order.includes(domainB)) {
typeB = this._getDomainType(domainB);
}
if (typeA === typeB) {
return stringCompare(
a[1].title,
b[1].title,
this.hass.locale.language
);
}
return order.indexOf(typeA) - order.indexOf(typeB);
})
.map((entry) => entry[1]);
}
// #endregion data
// #region data memoize
@@ -1368,12 +1420,12 @@ class DialogAddAutomationElement
private _getAreaEntityLookupMemoized = memoizeOne(
(entities: HomeAssistant["entities"]) =>
getAreaEntityLookup(Object.values(entities), true)
getAreaEntityLookup(Object.values(entities))
);
private _getDeviceEntityLookupMemoized = memoizeOne(
(entities: HomeAssistant["entities"]) =>
getDeviceEntityLookup(Object.values(entities), true)
getDeviceEntityLookup(Object.values(entities))
);
private _extractTypeAndIdFromTarget = memoizeOne(
@@ -1438,8 +1490,9 @@ class DialogAddAutomationElement
);
});
return Object.values(items).sort((a, b) =>
stringCompare(a.title, b.title, this.hass.locale.language)
return this._sortDomainsByCollection(
this._params!.type,
Object.entries(items)
);
}
@@ -1548,8 +1601,9 @@ class DialogAddAutomationElement
);
});
return Object.values(items).sort((a, b) =>
stringCompare(a.title, b.title, this.hass.locale.language)
return this._sortDomainsByCollection(
this._params!.type,
Object.entries(items)
);
}
@@ -1580,8 +1634,9 @@ class DialogAddAutomationElement
);
});
return Object.values(items).sort((a, b) =>
stringCompare(a.title, b.title, this.hass.locale.language)
return this._sortDomainsByCollection(
this._params!.type,
Object.entries(items)
);
}
@@ -1678,14 +1733,19 @@ class DialogAddAutomationElement
}
if (this._params!.type === "action") {
const items = await getServicesForTarget(
const items: string[] = await getServicesForTarget(
this.hass.callWS,
this._selectedTarget
);
const filteredItems = items.filter(
// homeassistant services are too generic to be applied on the selected target
(service) => !service.startsWith("homeassistant.")
);
this._targetItems = this._getDomainGroupedActionListItems(
this.hass.localize,
items
filteredItems
);
}
} catch (err) {
@@ -1913,7 +1973,7 @@ class DialogAddAutomationElement
ha-wa-dialog {
--dialog-content-padding: var(--ha-space-0);
--ha-dialog-min-height: min(
648px,
800px,
calc(
100vh - max(
var(--safe-area-inset-bottom),
@@ -1922,7 +1982,7 @@ class DialogAddAutomationElement
)
);
--ha-dialog-min-height: min(
648px,
800px,
calc(
100dvh - max(
var(--safe-area-inset-bottom),

View File

@@ -708,7 +708,11 @@ export default class HaAutomationAddFromTarget extends LitElement {
this.floors
);
const label = entityName || deviceName || entityId;
let label = entityName || deviceName || entityId;
if (this.entities[entityId]?.hidden) {
label += ` (${this.localize("ui.panel.config.automation.editor.entity_hidden")})`;
}
return [entityId, label, stateObj] as [string, string, HassEntity];
})
@@ -837,12 +841,12 @@ export default class HaAutomationAddFromTarget extends LitElement {
private _getAreaEntityLookupMemoized = memoizeOne(
(entities: HomeAssistant["entities"]) =>
getAreaEntityLookup(Object.values(entities), true)
getAreaEntityLookup(Object.values(entities))
);
private _getDeviceEntityLookupMemoized = memoizeOne(
(entities: HomeAssistant["entities"]) =>
getDeviceEntityLookup(Object.values(entities), true)
getDeviceEntityLookup(Object.values(entities))
);
private _getSelectedTargetId = memoizeOne(

View File

@@ -273,7 +273,7 @@ export class HaAutomationAddItems extends LitElement {
align-items: center;
color: var(--ha-color-text-secondary);
padding: var(--ha-space-0);
margin: var(--ha-space-3) var(--ha-space-4)
margin: var(--ha-space-0) var(--ha-space-4)
max(var(--safe-area-inset-bottom), var(--ha-space-3));
line-height: var(--ha-line-height-expanded);
justify-content: center;
@@ -306,7 +306,7 @@ export class HaAutomationAddItems extends LitElement {
.items .item-headline {
display: flex;
align-items: center;
gap: var(--ha-space-1);
gap: var(--ha-space-2);
min-height: var(--ha-space-9);
flex-wrap: wrap;
}
@@ -366,12 +366,16 @@ export class HaAutomationAddItems extends LitElement {
}
.selected-target state-badge {
--mdc-icon-size: 20px;
--mdc-icon-size: 24px;
}
.selected-target state-badge,
.selected-target ha-domain-icon {
.selected-target ha-floor-icon {
display: flex;
height: 32px;
width: 24px;
height: 24px;
align-items: center;
}
.selected-target ha-domain-icon {
filter: grayscale(100%);
}
`;

View File

@@ -393,6 +393,10 @@ export class HaPlatformCondition extends LitElement {
}
static styles = css`
:host {
display: block;
margin: 0px calc(-1 * var(--ha-space-4));
}
ha-settings-row {
padding: 0 var(--ha-space-4);
}

View File

@@ -35,6 +35,8 @@ import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify";
import "../../../components/ha-tooltip";
import type { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
@@ -327,14 +329,19 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
const date = new Date(automation.last_triggered);
const now = new Date();
const dayDifference = differenceInDays(now, date);
const formattedTime = formatShortDateTimeWithConditionalYear(
date,
this.hass.locale,
this.hass.config
);
const elementId = "last-triggered-" + slugify(automation.entity_id);
return html`
${dayDifference > 3
? formatShortDateTimeWithConditionalYear(
date,
this.hass.locale,
this.hass.config
)
: relativeTime(date, locale)}
? formattedTime
: html`
<ha-tooltip for=${elementId}>${formattedTime}</ha-tooltip>
<span id=${elementId}>${relativeTime(date, locale)}</span>
`}
`;
},
},

View File

@@ -429,6 +429,10 @@ export class HaPlatformTrigger extends LitElement {
}
static styles = css`
:host {
display: block;
margin: 0px calc(-1 * var(--ha-space-4));
}
ha-settings-row {
padding: 0 var(--ha-space-4);
}

View File

@@ -94,7 +94,7 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
back-path="/config/system"
.header=${this.hass.localize("ui.panel.config.labs.caption")}
>
${sortedFeatures.length

View File

@@ -24,7 +24,7 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
import { formatShortDateTimeWithConditionalYear } from "../../../common/datetime/format_date_time";
import { relativeTime } from "../../../common/datetime/relative_time";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
@@ -301,10 +301,21 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
const date = new Date(scene.state);
const now = new Date();
const dayDifference = differenceInDays(now, date);
const formattedTime = formatShortDateTimeWithConditionalYear(
date,
this.hass.locale,
this.hass.config
);
const elementId = "last-activated-" + slugify(scene.entity_id);
return html`
${dayDifference > 3
? formatShortDateTime(date, this.hass.locale, this.hass.config)
: relativeTime(date, this.hass.locale)}
? formattedTime
: html`
<ha-tooltip for=${elementId}>${formattedTime}</ha-tooltip>
<span id=${elementId}
>${relativeTime(date, this.hass.locale)}</span
>
`}
`;
},
},

View File

@@ -33,6 +33,8 @@ import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify";
import "../../../components/ha-tooltip";
import type { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
@@ -302,19 +304,27 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
sortable: true,
title: localize("ui.card.automation.last_triggered"),
template: (script) => {
if (!script.last_triggered) {
return this.hass.localize("ui.components.relative_time.never");
}
const date = new Date(script.last_triggered);
const now = new Date();
const dayDifference = differenceInDays(now, date);
const formattedTime = formatShortDateTimeWithConditionalYear(
date,
this.hass.locale,
this.hass.config
);
const elementId = "last-triggered-" + slugify(script.entity_id);
return html`
${script.last_triggered
? dayDifference > 3
? formatShortDateTimeWithConditionalYear(
date,
this.hass.locale,
this.hass.config
)
: relativeTime(date, this.hass.locale)
: this.hass.localize("ui.components.relative_time.never")}
${dayDifference > 3
? formattedTime
: html`
<ha-tooltip for=${elementId}>${formattedTime}</ha-tooltip>
<span id=${elementId}
>${relativeTime(date, this.hass.locale)}</span
>
`}
`;
},
},

View File

@@ -26,16 +26,24 @@ import type { LovelaceConfig } from "../../data/lovelace/config/types";
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
import type { StatisticValue } from "../../data/recorder";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import type { HomeAssistant, PanelInfo } from "../../types";
import { fileDownload } from "../../util/file_download";
import "../lovelace/components/hui-energy-period-selector";
import "../lovelace/hui-root";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
export const DEFAULT_ENERGY_COLLECTION_KEY = "energy_dashboard";
const EMPTY_PREFERENCES: EnergyPreferences = {
energy_sources: [],
device_consumption: [],
device_consumption_water: [],
};
const OVERVIEW_VIEW = {
path: "overview",
strategy: {
type: "energy-overview",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
@@ -43,8 +51,8 @@ const OVERVIEW_VIEW = {
} as LovelaceViewConfig;
const ELECTRICITY_VIEW = {
back_path: "/energy",
path: "electricity",
back_path: "/energy",
strategy: {
type: "energy-electricity",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
@@ -72,54 +80,96 @@ class PanelEnergy extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ attribute: false }) public panel?: PanelInfo;
@state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _error?: string;
@property({ attribute: false }) public route?: {
path: string;
prefix: string;
};
@state()
private _config?: LovelaceConfig;
private _prefs?: EnergyPreferences;
get _viewPath(): string | undefined {
const viewPath: string | undefined = this.route!.path.split("/")[1];
return viewPath ? decodeURI(viewPath) : undefined;
}
@state()
private _error?: string;
public connectedCallback() {
super.connectedCallback();
this._loadLovelaceConfig();
}
public async willUpdate(changedProps: PropertyValues) {
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
// Initial setup
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
this._loadConfig();
return;
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass?.locale !== this.hass.locale) {
if (oldHass && oldHass.localize !== this.hass.localize) {
this._setLovelace();
} else if (oldHass && oldHass.localize !== this.hass.localize) {
this._reloadView();
}
}
private async _loadLovelaceConfig() {
private _fetchEnergyPrefs = async (): Promise<
EnergyPreferences | undefined
> => {
const collection = getEnergyDataCollection(this.hass, {
key: DEFAULT_ENERGY_COLLECTION_KEY,
});
try {
this._config = undefined;
this._config = await this._generateLovelaceConfig();
} catch (err) {
this._error = (err as Error).message;
await collection.refresh();
} catch (err: any) {
if (err.code === "not_found") {
return undefined;
}
throw err;
}
return collection.prefs;
};
this._setLovelace();
private async _loadConfig() {
try {
this._error = undefined;
const prefs = await this._fetchEnergyPrefs();
this._prefs = prefs || EMPTY_PREFERENCES;
} catch (err) {
// eslint-disable-next-line no-console
console.error("Failed to load prefs:", err);
this._prefs = EMPTY_PREFERENCES;
this._error = (err as Error).message || "Unknown error";
}
await this._setLovelace();
// Navigate to first view if not there yet
const firstPath = this._lovelace!.config?.views?.[0]?.path;
const viewPath: string | undefined = this.route!.path.split("/")[1];
if (viewPath !== firstPath) {
navigate(`${this.route!.prefix}/${firstPath}`);
}
}
private async _setLovelace() {
const config = await this._generateLovelaceConfig();
this._lovelace = {
config: config,
rawConfig: config,
editMode: false,
urlPath: "energy",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
private _back(ev) {
@@ -128,7 +178,17 @@ class PanelEnergy extends LitElement {
}
protected render() {
if (!this._config && !this._error) {
if (this._error) {
return html`
<div class="centered">
<ha-alert alert-type="error">
An error occurred loading energy preferences: ${this._error}
</ha-alert>
</div>
`;
}
if (!this._prefs) {
// Still loading
return html`
<div class="centered">
@@ -136,20 +196,31 @@ class PanelEnergy extends LitElement {
</div>
`;
}
const isSingleView = this._config?.views.length === 1;
const viewPath = this._viewPath;
const viewIndex = this._config
? Math.max(
this._config.views.findIndex((view) => view.path === viewPath),
0
)
: 0;
const showBack =
this._searchParms.has("historyBack") || (!isSingleView && viewIndex > 0);
if (!this._lovelace) {
return nothing;
}
const viewPath: string | undefined = this.route!.path.split("/")[1];
const views = this._lovelace.config?.views || [];
const viewIndex = Math.max(
views.findIndex((view) => view.path === viewPath),
0
);
const showBack = this._searchParms.has("historyBack") || viewIndex > 0;
return html`
<div class="header">
<div class="toolbar">
<hui-root
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.route=${this.route}
.panel=${this.panel}
@reload-energy-panel=${this._reloadConfig}
>
<div class="toolbar" slot="toolbar">
${showBack
? html`
<ha-icon-button-arrow-prev
@@ -175,14 +246,17 @@ class PanelEnergy extends LitElement {
.collectionKey=${DEFAULT_ENERGY_COLLECTION_KEY}
>
${this.hass.user?.is_admin
? html` <ha-list-item
slot="overflow-menu"
graphic="icon"
@request-selected=${this._navigateConfig}
>
<ha-svg-icon slot="graphic" .path=${mdiPencil}> </ha-svg-icon>
${this.hass!.localize("ui.panel.energy.configure")}
</ha-list-item>`
? html`
<ha-list-item
slot="overflow-menu"
graphic="icon"
@request-selected=${this._navigateConfig}
>
<ha-svg-icon slot="graphic" .path=${mdiPencil}>
</ha-svg-icon>
${this.hass!.localize("ui.panel.energy.configure")}
</ha-list-item>
`
: nothing}
<ha-list-item
slot="overflow-menu"
@@ -194,54 +268,15 @@ class PanelEnergy extends LitElement {
</ha-list-item>
</hui-energy-period-selector>
</div>
</div>
<hui-view-container
.hass=${this.hass}
@reload-energy-panel=${this._reloadView}
>
${this._error
? html`<div class="centered">
<ha-alert alert-type="error">
An error occurred while fetching your energy preferences:
${this._error}
</ha-alert>
</div>`
: this._lovelace
? html`<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${viewIndex}
></hui-view>`
: nothing}
</hui-view-container>
</hui-root>
`;
}
private _fetchEnergyPrefs = async (): Promise<
EnergyPreferences | undefined
> => {
const collection = getEnergyDataCollection(this.hass, {
key: DEFAULT_ENERGY_COLLECTION_KEY,
});
try {
await collection.refresh();
} catch (err: any) {
if (err.code === "not_found") {
return undefined;
}
throw err;
}
return collection.prefs;
};
private async _generateLovelaceConfig(): Promise<LovelaceConfig> {
const prefs = await this._fetchEnergyPrefs();
if (
!prefs ||
(prefs.device_consumption.length === 0 &&
prefs.energy_sources.length === 0)
!this._prefs ||
(this._prefs.device_consumption.length === 0 &&
this._prefs.energy_sources.length === 0)
) {
await import("./cards/energy-setup-wizard-card");
return {
@@ -249,7 +284,7 @@ class PanelEnergy extends LitElement {
};
}
const isElectricityOnly = prefs.energy_sources.every((source) =>
const isElectricityOnly = this._prefs.energy_sources.every((source) =>
["grid", "solar", "battery"].includes(source.type)
);
if (isElectricityOnly) {
@@ -259,8 +294,8 @@ class PanelEnergy extends LitElement {
}
const hasWater =
prefs.energy_sources.some((source) => source.type === "water") ||
prefs.device_consumption_water?.length > 0;
this._prefs.energy_sources.some((source) => source.type === "water") ||
this._prefs.device_consumption_water?.length > 0;
const views: LovelaceViewConfig[] = [OVERVIEW_VIEW, ELECTRICITY_VIEW];
if (hasWater) {
@@ -269,25 +304,6 @@ class PanelEnergy extends LitElement {
return { views };
}
private _setLovelace() {
if (!this._config) {
return;
}
this._lovelace = {
config: this._config,
rawConfig: this._config,
editMode: false,
urlPath: "energy",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
private _navigateConfig(ev) {
ev.stopPropagation();
navigate("/config/energy?historyBack=1");
@@ -593,8 +609,8 @@ class PanelEnergy extends LitElement {
fileDownload(url, "energy.csv");
}
private _reloadView() {
this._loadLovelaceConfig();
private _reloadConfig() {
this._loadConfig();
}
static get styles(): CSSResultGroup {
@@ -620,45 +636,6 @@ class PanelEnergy extends LitElement {
-webkit-user-select: none;
-moz-user-select: none;
}
.header {
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
position: fixed;
top: 0;
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-right,
0px
)
);
padding-top: var(--safe-area-inset-top);
z-index: 4;
transition: box-shadow 200ms linear;
display: flex;
flex-direction: row;
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
backdrop-filter: var(--app-header-backdrop-filter, none);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
}
:host([narrow]) .header {
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-left,
0px
) - var(--safe-area-inset-right, 0px)
);
padding-left: var(--safe-area-inset-left);
}
:host([scrolled]) .header {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.toolbar {
height: var(--header-height);
display: flex;
@@ -677,24 +654,6 @@ class PanelEnergy extends LitElement {
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
hui-view-container {
position: relative;
display: flex;
min-height: 100vh;
box-sizing: border-box;
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
padding-right: var(--safe-area-inset-right);
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
}
:host([narrow]) hui-view-container {
padding-left: var(--safe-area-inset-left);
padding-inline-start: var(--safe-area-inset-left);
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
}
.centered {
width: 100%;
height: 100%;

View File

@@ -55,6 +55,9 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
const hasPowerDevices = prefs.device_consumption.find(
(device) => device.stat_rate
);
const showFloorsNAreas = !prefs.device_consumption.some(
(d) => d.included_in_stat
);
view.cards!.push({
type: "energy-compare",
@@ -67,6 +70,8 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
title: hass.localize("ui.panel.energy.cards.power_sankey_title"),
type: "power-sankey",
collection_key: collectionKey,
group_by_floor: showFloorsNAreas,
group_by_area: showFloorsNAreas,
grid_options: {
columns: 24,
},
@@ -156,9 +161,6 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
// Only include if we have at least 1 device in the config.
if (prefs.device_consumption.length) {
const showFloorsNAreas = !prefs.device_consumption.some(
(d) => d.included_in_stat
);
view.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_sankey_title"),
type: "energy-sankey",

View File

@@ -81,10 +81,15 @@ export class EnergyViewStrategy extends ReactiveElement {
cards: [],
};
if (hasPowerSources && hasPowerDevices) {
const showFloorsNAreas = !prefs.device_consumption.some(
(d) => d.included_in_stat
);
overviewSection.cards!.push({
title: hass.localize("ui.panel.energy.cards.power_sankey_title"),
type: "power-sankey",
collection_key: collectionKey,
group_by_floor: showFloorsNAreas,
group_by_area: showFloorsNAreas,
grid_options: {
columns: 24,
},

View File

@@ -2,6 +2,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entities-picker";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-wa-dialog";
@@ -78,6 +79,16 @@ export class DialogEditHome
@value-changed=${this._favoriteEntitiesChanged}
></ha-entities-picker>
<ha-alert alert-type="info">
${this.hass.localize("ui.panel.home.editor.areas_hint", {
areas_page: html`<a
href="/config/areas?historyBack=1"
@click=${this.closeDialog}
>${this.hass.localize("ui.panel.home.editor.areas_page")}</a
>`,
})}
</ha-alert>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
@@ -140,6 +151,11 @@ export class DialogEditHome
ha-entities-picker {
display: block;
}
ha-alert {
display: block;
margin-top: var(--ha-space-4);
}
`,
];
}

View File

@@ -217,6 +217,9 @@ export class HuiEnergyDevicesGraphCard
show: true,
type: "value",
name: "kWh",
axisPointer: {
show: false,
},
};
options.yAxis = {
show: true,

View File

@@ -23,6 +23,9 @@ const DEFAULT_CONFIG: Partial<PowerSankeyCardConfig> = {
group_by_area: true,
};
// Minimum power threshold in kW to display a device node
const MIN_POWER_THRESHOLD = 0.01;
interface PowerData {
solar: number;
from_grid: number;
@@ -251,23 +254,75 @@ class HuiPowerSankeyCard
let untrackedConsumption = homeNode.value;
const deviceNodes: Node[] = [];
const parentLinks: Record<string, string> = {};
// Build a map of device relationships for hierarchy resolution
// Key: stat_consumption (energy), Value: { stat_rate, included_in_stat }
const deviceMap = new Map<
string,
{ stat_rate?: string; included_in_stat?: string }
>();
prefs.device_consumption.forEach((device) => {
deviceMap.set(device.stat_consumption, {
stat_rate: device.stat_rate,
included_in_stat: device.included_in_stat,
});
});
// Set of stat_rate entities that will be rendered as nodes
const renderedStatRates = new Set<string>();
prefs.device_consumption.forEach((device) => {
if (device.stat_rate) {
const value = this._getCurrentPower(device.stat_rate);
if (value >= MIN_POWER_THRESHOLD) {
renderedStatRates.add(device.stat_rate);
}
}
});
// Find the effective parent for power hierarchy
// Walks up the chain to find an ancestor with stat_rate that will be rendered
const findEffectiveParent = (
includedInStat: string | undefined
): string | undefined => {
let currentParent = includedInStat;
while (currentParent) {
const parentDevice = deviceMap.get(currentParent);
if (!parentDevice) {
return undefined;
}
// If this parent has a stat_rate and will be rendered, use it
if (
parentDevice.stat_rate &&
renderedStatRates.has(parentDevice.stat_rate)
) {
return parentDevice.stat_rate;
}
// Otherwise, continue up the chain
currentParent = parentDevice.included_in_stat;
}
return undefined;
};
prefs.device_consumption.forEach((device, idx) => {
if (!device.stat_rate) {
return;
}
const value = this._getCurrentPower(device.stat_rate);
if (value < 0.01) {
if (value < MIN_POWER_THRESHOLD) {
return;
}
// Find the effective parent (may be different from direct parent if parent has no stat_rate)
const effectiveParent = findEffectiveParent(device.included_in_stat);
const node = {
id: device.stat_rate,
label: device.name || this._getEntityLabel(device.stat_rate),
value,
color: getGraphColorByIndex(idx, computedStyle),
index: 4,
parent: device.included_in_stat,
parent: effectiveParent,
};
if (node.parent) {
parentLinks[node.id] = node.parent;

View File

@@ -98,17 +98,32 @@ class HuiWaterSankeyCard
const nodes: Node[] = [];
const links: Link[] = [];
// Calculate total water consumption from all devices
let totalWaterConsumption = 0;
prefs.device_consumption_water.forEach((device) => {
// Calculate total water consumption from all sources or devices
const totalDownstreamConsumption = prefs.device_consumption_water.reduce(
(total, device) => {
const value =
device.stat_consumption in this._data!.stats
? calculateStatisticSumGrowth(
this._data!.stats[device.stat_consumption]
) || 0
: 0;
return total + value;
},
0
);
const totalSourceSupply = waterSources.reduce((total, source) => {
const value =
device.stat_consumption in this._data!.stats
source.stat_energy_from in this._data!.stats
? calculateStatisticSumGrowth(
this._data!.stats[device.stat_consumption]
this._data!.stats[source.stat_energy_from]
) || 0
: 0;
totalWaterConsumption += value;
});
return total + value;
}, 0);
const totalWaterConsumption = Math.max(
totalDownstreamConsumption,
totalSourceSupply
);
// Create home/consumption node
const homeNode: Node = {

View File

@@ -127,7 +127,7 @@ interface UndoStackItem {
@customElement("hui-root")
class HUIRoot extends LitElement {
@property({ attribute: false }) public panel?: PanelInfo<LovelacePanelConfig>;
@property({ attribute: false }) public panel?: PanelInfo;
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -543,68 +543,72 @@ class HUIRoot extends LitElement {
})}
>
<div class="header">
<div class="toolbar">
<slot name="toolbar">
<div class="toolbar">
${this._editMode
? html`
<div class="main-title">
${dashboardTitle ||
this.hass!.localize("ui.panel.lovelace.editor.header")}
<ha-icon-button
slot="actionItems"
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_lovelace.edit_title"
)}
.path=${mdiPencil}
class="edit-icon"
@click=${this._editDashboard}
></ha-icon-button>
</div>
<div class="action-items">${this._renderActionItems()}</div>
`
: html`
${isSubview
? html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
slot="navigationIcon"
@click=${this._goBack}
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
${isSubview
? html`
<div class="main-title">${curViewConfig.title}</div>
`
: hasTabViews
? tabs
: html`
<div class="main-title">
${views[0]?.title ?? dashboardTitle}
</div>
`}
<div class="action-items">${this._renderActionItems()}</div>
`}
</div>
${this._editMode
? html`
<div class="main-title">
${dashboardTitle ||
this.hass!.localize("ui.panel.lovelace.editor.header")}
<div class="tab-bar">
${tabs}
<ha-icon-button
slot="actionItems"
slot="nav"
id="add-view"
@click=${this._addView}
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_lovelace.edit_title"
"ui.panel.lovelace.editor.edit_view.add"
)}
.path=${mdiPencil}
class="edit-icon"
@click=${this._editDashboard}
.path=${mdiPlus}
></ha-icon-button>
</div>
<div class="action-items">${this._renderActionItems()}</div>
`
: html`
${isSubview
? html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
slot="navigationIcon"
@click=${this._goBack}
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
${isSubview
? html`<div class="main-title">${curViewConfig.title}</div>`
: hasTabViews
? tabs
: html`
<div class="main-title">
${views[0]?.title ?? dashboardTitle}
</div>
`}
<div class="action-items">${this._renderActionItems()}</div>
`}
</div>
${this._editMode
? html`
<div class="tab-bar">
${tabs}
<ha-icon-button
slot="nav"
id="add-view"
@click=${this._addView}
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.add"
)}
.path=${mdiPlus}
></ha-icon-button>
</div>
`
: nothing}
: nothing}
</slot>
</div>
<hui-view-container
class=${this._editMode ? "has-tab-bar" : ""}

View File

@@ -123,6 +123,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
"section-visibility-changed",
this._sectionVisibilityChanged
);
this._showSidebar = Boolean(window.history.state?.sidebar);
}
disconnectedCallback(): void {
@@ -428,6 +429,12 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
this._showSidebar = !this._showSidebar;
// Add sidebar state to history
window.history.replaceState(
{ ...window.history.state, sidebar: this._showSidebar },
""
);
// Restore scroll position after view updates
this.updateComplete.then(() => {
const scrollY = this._showSidebar
@@ -507,8 +514,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
.wrapper.narrow hui-view-sidebar {
grid-column: 1 / -1;
padding-bottom: calc(
var(--ha-space-4) + 56px + var(--ha-space-4) +
env(safe-area-inset-bottom)
var(--ha-space-14) + var(--ha-space-3) + var(--safe-area-inset-bottom)
);
}
@@ -518,25 +524,24 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
.mobile-tabs {
position: fixed;
bottom: calc(var(--ha-space-4) + env(safe-area-inset-bottom));
bottom: calc(var(--ha-space-3) + var(--safe-area-inset-bottom));
left: 50%;
transform: translateX(-50%);
padding: 0;
z-index: 1;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.15))
drop-shadow(0 4px 16px rgba(0, 0, 0, 0.1));
}
.mobile-tabs ha-control-select {
width: max-content;
min-width: 280px;
max-width: 90%;
--control-select-thickness: 56px;
--control-select-border-radius: var(--ha-border-radius-6xl);
--control-select-thickness: var(--ha-space-14);
--control-select-border-radius: var(--ha-border-radius-pill);
--control-select-background: var(--card-background-color);
--control-select-background-opacity: 1;
--control-select-color: var(--primary-color);
--control-select-padding: 6px;
box-shadow: rgba(0, 0, 0, 0.3) 0px 4px 10px 0px;
}
ha-sortable {
@@ -560,8 +565,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
.wrapper.narrow.has-sidebar .content {
padding-bottom: calc(
var(--ha-space-4) + 56px + var(--ha-space-4) +
env(safe-area-inset-bottom)
var(--ha-space-14) + var(--ha-space-3) + var(--safe-area-inset-bottom)
);
}

View File

@@ -1,14 +1,17 @@
import { mdiViewDashboard } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-divider";
import "../../components/ha-icon";
import "../../components/ha-list-item";
import "../../components/ha-select";
import "../../components/ha-settings-row";
import "../../components/ha-svg-icon";
import { saveFrontendUserData } from "../../data/frontend";
import type { LovelaceDashboard } from "../../data/lovelace/dashboard";
import { fetchDashboards } from "../../data/lovelace/dashboard";
import { getPanelTitle } from "../../data/panel";
import { getPanelIcon, getPanelTitle } from "../../data/panel";
import type { HomeAssistant, PanelInfo } from "../../types";
import { PANEL_DASHBOARDS } from "../config/lovelace/dashboards/ha-config-lovelace-dashboards";
@@ -37,54 +40,57 @@ class HaPickDashboardRow extends LitElement {
<span slot="description">
${this.hass.localize("ui.panel.profile.dashboard.description")}
</span>
${this._dashboards
? html`<ha-select
.label=${this.hass.localize(
"ui.panel.profile.dashboard.dropdown_label"
)}
.disabled=${!this._dashboards?.length}
.value=${value}
@selected=${this._dashboardChanged}
naturalMenuWidth
>
<ha-list-item .value=${USE_SYSTEM_VALUE}>
${this.hass.localize("ui.panel.profile.dashboard.system")}
<ha-select
.label=${this.hass.localize(
"ui.panel.profile.dashboard.dropdown_label"
)}
.value=${value}
@selected=${this._dashboardChanged}
naturalMenuWidth
>
<ha-list-item .value=${USE_SYSTEM_VALUE}>
${this.hass.localize("ui.panel.profile.dashboard.system")}
</ha-list-item>
<ha-divider></ha-divider>
<ha-list-item value="lovelace" graphic="icon">
<ha-svg-icon slot="graphic" .path=${mdiViewDashboard}></ha-svg-icon>
${this.hass.localize("ui.panel.profile.dashboard.lovelace")}
</ha-list-item>
${PANEL_DASHBOARDS.map((panel) => {
const panelInfo = this.hass.panels[panel] as PanelInfo | undefined;
if (!panelInfo) {
return nothing;
}
return html`
<ha-list-item value=${panelInfo.url_path} graphic="icon">
<ha-icon
slot="graphic"
.icon=${getPanelIcon(panelInfo)}
></ha-icon>
${getPanelTitle(this.hass, panelInfo)}
</ha-list-item>
<ha-divider></ha-divider>
<ha-list-item value="lovelace">
${this.hass.localize("ui.panel.profile.dashboard.lovelace")}
</ha-list-item>
${PANEL_DASHBOARDS.map((panel) => {
const panelInfo = this.hass.panels[panel] as
| PanelInfo
| undefined;
if (!panelInfo) {
return nothing;
}
return html`
<ha-list-item value=${panelInfo.url_path}>
${getPanelTitle(this.hass, panelInfo)}
</ha-list-item>
`;
})}
<ha-divider></ha-divider>
${this._dashboards.map((dashboard) => {
if (!this.hass.user!.is_admin && dashboard.require_admin) {
return "";
}
return html`
<ha-list-item .value=${dashboard.url_path}>
${dashboard.title}
</ha-list-item>
`;
})}
</ha-select>`
: html`<ha-select
.label=${this.hass.localize(
"ui.panel.profile.dashboard.dropdown_label"
)}
disabled
></ha-select>`}
`;
})}
${this._dashboards?.length
? html`
<ha-divider></ha-divider>
${this._dashboards.map((dashboard) => {
if (!this.hass.user!.is_admin && dashboard.require_admin) {
return "";
}
return html`
<ha-list-item .value=${dashboard.url_path} graphic="icon">
<ha-icon
slot="graphic"
.icon=${dashboard.icon || "mdi:view-dashboard"}
></ha-icon>
${dashboard.title}
</ha-list-item>
`;
})}
`
: nothing}
</ha-select>
</ha-settings-row>
`;
}

View File

@@ -2227,7 +2227,9 @@
"title": "Edit home page",
"description": "Configure your home page display preferences.",
"favorite_entities_helper": "Display your favorite entities. Home Assistant will still suggest based on commonly used up to 8 slots.",
"save_failed": "Failed to save home page configuration"
"save_failed": "Failed to save home page configuration",
"areas_hint": "You can rearrange your floors and areas in the order that best represents your house on the {areas_page}.",
"areas_page": "areas page"
}
},
"my": {
@@ -4045,6 +4047,7 @@
"other_areas": "Other areas",
"services": "Services",
"helpers": "Helpers",
"entity_hidden": "[%key:ui::panel::config::devices::entities::hidden%]",
"triggers": {
"name": "Triggers",
"header": "When",

View File

@@ -1,8 +1,14 @@
import { afterEach, describe, expect, test, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
let askWrite;
const HASS_URL = `${location.protocol}//${location.host}`;
describe("token_storage.askWrite", () => {
beforeEach(() => {
vi.stubGlobal("__HASS_URL__", HASS_URL);
});
afterEach(() => {
vi.resetModules();
});

View File

@@ -4,9 +4,12 @@ import { FallbackStorage } from "../../../test_helper/local-storage-fallback";
let saveTokens;
const HASS_URL = `${location.protocol}//${location.host}`;
describe("token_storage.saveTokens", () => {
beforeEach(() => {
window.localStorage = new FallbackStorage();
vi.stubGlobal("__HASS_URL__", HASS_URL);
});
afterEach(() => {

View File

@@ -2,6 +2,8 @@ import { describe, it, expect, test, vi, afterEach, beforeEach } from "vitest";
import type { AuthData } from "home-assistant-js-websocket";
import { FallbackStorage } from "../../../test_helper/local-storage-fallback";
const HASS_URL = `${location.protocol}//${location.host}`;
describe("token_storage", () => {
beforeEach(() => {
vi.stubGlobal(
@@ -11,6 +13,7 @@ describe("token_storage", () => {
writeEnabled: undefined,
})
);
vi.stubGlobal("__HASS_URL__", HASS_URL);
window.localStorage = new FallbackStorage();
});

151
yarn.lock
View File

@@ -4945,140 +4945,139 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.47.0"
"@typescript-eslint/eslint-plugin@npm:8.48.0":
version: 8.48.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.48.0"
dependencies:
"@eslint-community/regexpp": "npm:^4.10.0"
"@typescript-eslint/scope-manager": "npm:8.47.0"
"@typescript-eslint/type-utils": "npm:8.47.0"
"@typescript-eslint/utils": "npm:8.47.0"
"@typescript-eslint/visitor-keys": "npm:8.47.0"
"@typescript-eslint/scope-manager": "npm:8.48.0"
"@typescript-eslint/type-utils": "npm:8.48.0"
"@typescript-eslint/utils": "npm:8.48.0"
"@typescript-eslint/visitor-keys": "npm:8.48.0"
graphemer: "npm:^1.4.0"
ignore: "npm:^7.0.0"
natural-compare: "npm:^1.4.0"
ts-api-utils: "npm:^2.1.0"
peerDependencies:
"@typescript-eslint/parser": ^8.47.0
"@typescript-eslint/parser": ^8.48.0
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/53d86116a39429c0cdde5969f9ea2cf712f7c7cb2ed023088a876686a4771df131dbefda7645ba0724e2a5a0532e14bdffcb92c708060a46a8607dc5243083d1
checksum: 10/c9cd87c72da7bb7f6175fdb53a4c08a26e61a3d9d1024960d193276217b37ca1e8e12328a57751ed9380475e11e198f9715e172126ea7d3b3da9948d225db92b
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/parser@npm:8.47.0"
"@typescript-eslint/parser@npm:8.48.0":
version: 8.48.0
resolution: "@typescript-eslint/parser@npm:8.48.0"
dependencies:
"@typescript-eslint/scope-manager": "npm:8.47.0"
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/typescript-estree": "npm:8.47.0"
"@typescript-eslint/visitor-keys": "npm:8.47.0"
"@typescript-eslint/scope-manager": "npm:8.48.0"
"@typescript-eslint/types": "npm:8.48.0"
"@typescript-eslint/typescript-estree": "npm:8.48.0"
"@typescript-eslint/visitor-keys": "npm:8.48.0"
debug: "npm:^4.3.4"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/e7213296a27b78511b8c9b2627ff6530d0eb31e6b076eef6f34f11ca7fbcb7e998a2fa079bfc1563a53f9d88326aa4af995241bcdf08a15c3e5be0d13fdff2d7
checksum: 10/5919642345c79a43e57a85e0e69d1f56b5756b3fdb3586ec6371969604f589adc188338c8f12a787456edc3b38c70586d8209cffcf45e35e5a5ebd497c5f4257
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/project-service@npm:8.47.0"
"@typescript-eslint/project-service@npm:8.48.0":
version: 8.48.0
resolution: "@typescript-eslint/project-service@npm:8.48.0"
dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.47.0"
"@typescript-eslint/types": "npm:^8.47.0"
"@typescript-eslint/tsconfig-utils": "npm:^8.48.0"
"@typescript-eslint/types": "npm:^8.48.0"
debug: "npm:^4.3.4"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/e2f935dae66ce27e6c0cce8b750da0e8fe84b6e0fa248bf8210b84eec3c4d2e2679a878185f445ce507d132215a676dcf8a21d47ab70c547da47ede000a128e1
checksum: 10/5853a2f57bf8a26b70c1fe5a906c1890ad4f0fca127218a7805161fc9ad547af97f4a600f32f5acdf2f2312b156affca2bea84af9a433215cbcc2056b6a27c77
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/scope-manager@npm:8.47.0"
"@typescript-eslint/scope-manager@npm:8.48.0":
version: 8.48.0
resolution: "@typescript-eslint/scope-manager@npm:8.48.0"
dependencies:
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/visitor-keys": "npm:8.47.0"
checksum: 10/e97ae0f746f6bb5706181a973bcc0c1268706ef7e8c18594b37168bb0b41b1673d3f0ba1a2575ee3bd121066500fdc75af313f6ad283198942a5cdb65ade7621
"@typescript-eslint/types": "npm:8.48.0"
"@typescript-eslint/visitor-keys": "npm:8.48.0"
checksum: 10/963af7af235e940467504969c565b359ca454a156eba0d5af2e4fd9cca4294947187e1a85107ff05801688ac85b5767d2566414cbef47a03c23f7b46527decca
languageName: node
linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.47.0, @typescript-eslint/tsconfig-utils@npm:^8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.47.0"
"@typescript-eslint/tsconfig-utils@npm:8.48.0, @typescript-eslint/tsconfig-utils@npm:^8.48.0":
version: 8.48.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.48.0"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/7f44441da3778928937419f8ebc62939538cf30087e56c0ca56f599ce98111b82f496902a9e15d713822b9cd14b17937d57b722468450a48748f8e50fd7161af
checksum: 10/e480cd80498c4119a8c5bc413a22abf4bf365b3674ff95f5513292ede31e4fd8118f50d76a786de702696396a43c0c7a4d0c2ccd1c2c7db61bd941ba74495021
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/type-utils@npm:8.47.0"
"@typescript-eslint/type-utils@npm:8.48.0":
version: 8.48.0
resolution: "@typescript-eslint/type-utils@npm:8.48.0"
dependencies:
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/typescript-estree": "npm:8.47.0"
"@typescript-eslint/utils": "npm:8.47.0"
"@typescript-eslint/types": "npm:8.48.0"
"@typescript-eslint/typescript-estree": "npm:8.48.0"
"@typescript-eslint/utils": "npm:8.48.0"
debug: "npm:^4.3.4"
ts-api-utils: "npm:^2.1.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/07dcdd1ac071bbaf87b6b320d107129787a62cc403ce78e081cbe5e2ed0c576d660654e4117e6224c4c23d46919d7130b70801835d2fc41d9344c47ff946ce81
checksum: 10/dfda42624d534f9fed270bd5c76c9c0bb879cccd3dfbfc2977c84489860fbc204f10bca5c69f3ac856cc4342c12f8947293e7449d3391af289620d7ec79ced0d
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.47.0, @typescript-eslint/types@npm:^8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/types@npm:8.47.0"
checksum: 10/fc42416c01c512cfe1533bdf521925bca999adc68ffefa246e48552783f1fe9d22487d912611c5cb35fca481604aae3cab88279a53ce76c7cd7510b76775c078
"@typescript-eslint/types@npm:8.48.0, @typescript-eslint/types@npm:^8.48.0":
version: 8.48.0
resolution: "@typescript-eslint/types@npm:8.48.0"
checksum: 10/cd14a7ecd1cb6af94e059a713357b9521ffab08b2793a7d33abda7006816e77f634d49d1ec6f1b99b47257a605347d691bd02b2b11477c9c328f2a27f52a664f
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/typescript-estree@npm:8.47.0"
"@typescript-eslint/typescript-estree@npm:8.48.0":
version: 8.48.0
resolution: "@typescript-eslint/typescript-estree@npm:8.48.0"
dependencies:
"@typescript-eslint/project-service": "npm:8.47.0"
"@typescript-eslint/tsconfig-utils": "npm:8.47.0"
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/visitor-keys": "npm:8.47.0"
"@typescript-eslint/project-service": "npm:8.48.0"
"@typescript-eslint/tsconfig-utils": "npm:8.48.0"
"@typescript-eslint/types": "npm:8.48.0"
"@typescript-eslint/visitor-keys": "npm:8.48.0"
debug: "npm:^4.3.4"
fast-glob: "npm:^3.3.2"
is-glob: "npm:^4.0.3"
minimatch: "npm:^9.0.4"
semver: "npm:^7.6.0"
tinyglobby: "npm:^0.2.15"
ts-api-utils: "npm:^2.1.0"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/a480e83f1fca8a389642cbb18855ef25214c4765694b1d4a74051d2653a4fbbbf3a3cc4e544d1ecb79d49958fbf819246043c0d823d4384aa1c7b5ff79d02fcc
checksum: 10/8ee6b9e98dd72d567b8842a695578b2098bd8cdcf5628d2819407a52b533a5a139ba9a5620976641bc4553144a1b971d75f2df218a7c281fe674df25835e9e22
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/utils@npm:8.47.0"
"@typescript-eslint/utils@npm:8.48.0":
version: 8.48.0
resolution: "@typescript-eslint/utils@npm:8.48.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.7.0"
"@typescript-eslint/scope-manager": "npm:8.47.0"
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/typescript-estree": "npm:8.47.0"
"@typescript-eslint/scope-manager": "npm:8.48.0"
"@typescript-eslint/types": "npm:8.48.0"
"@typescript-eslint/typescript-estree": "npm:8.48.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/e165bbcaaafb88761f12272bc4b3be1631d8a8ea319765c80cfe5bf7a5858f437486eeae177643baa213570a664f0254b41bf0541e9238b57080bb30d1a2c8ab
checksum: 10/980b9faeaae0357bd7c002b15ab3bbcb7d5e4558be5df7980cf5221b41570a1a7b7d71ea2fcc8b1387f6c0db948d01468e6dcb31230d6757e28ac2ee5d8be4cf
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/visitor-keys@npm:8.47.0"
"@typescript-eslint/visitor-keys@npm:8.48.0":
version: 8.48.0
resolution: "@typescript-eslint/visitor-keys@npm:8.48.0"
dependencies:
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/types": "npm:8.48.0"
eslint-visitor-keys: "npm:^4.2.1"
checksum: 10/1e184cdebc4ab15da8a46ae2624ba4543c6bea83ced80a1602da99b72c00b5f6ea913ae021823c555a35a65bb9a9df09d119713998c44b00eba25e1407844294
checksum: 10/f9eaff8225b3b00e486e0221bd596b08a3ed463f31fab88221256908f6208c48f745281b7b92e6358d25e1dbdc37c6c2f4b42503403c24b071165bafd9a35d52
languageName: node
linkType: hard
@@ -8347,7 +8346,7 @@ __metadata:
languageName: node
linkType: hard
"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.2, fast-glob@npm:^3.3.2, fast-glob@npm:^3.3.3":
"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.2, fast-glob@npm:^3.3.3":
version: 3.3.3
resolution: "fast-glob@npm:3.3.3"
dependencies:
@@ -9368,7 +9367,7 @@ __metadata:
tinykeys: "npm:3.0.0"
ts-lit-plugin: "npm:2.0.2"
typescript: "npm:5.9.3"
typescript-eslint: "npm:8.47.0"
typescript-eslint: "npm:8.48.0"
ua-parser-js: "npm:2.0.6"
vite-tsconfig-paths: "npm:5.1.4"
vitest: "npm:4.0.13"
@@ -14281,18 +14280,18 @@ __metadata:
languageName: node
linkType: hard
"typescript-eslint@npm:8.47.0":
version: 8.47.0
resolution: "typescript-eslint@npm:8.47.0"
"typescript-eslint@npm:8.48.0":
version: 8.48.0
resolution: "typescript-eslint@npm:8.48.0"
dependencies:
"@typescript-eslint/eslint-plugin": "npm:8.47.0"
"@typescript-eslint/parser": "npm:8.47.0"
"@typescript-eslint/typescript-estree": "npm:8.47.0"
"@typescript-eslint/utils": "npm:8.47.0"
"@typescript-eslint/eslint-plugin": "npm:8.48.0"
"@typescript-eslint/parser": "npm:8.48.0"
"@typescript-eslint/typescript-estree": "npm:8.48.0"
"@typescript-eslint/utils": "npm:8.48.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/159dad98535dafd68c6228fae4aaf9e02d65d9ac3b02ddf0356b56ce72651dd9860e8bf9e0ee22532d77dd382d7f810e1bf1dd41c3fab381f627a08580c5117e
checksum: 10/9be54df60faf3b5a6d255032b4478170b6f64e38b8396475a2049479d1e3c1f5a23a18bb4d2d6ff685ef92ff8f2af28215772fe33b48148a8cf83a724d0778d1
languageName: node
linkType: hard