${this.localize("network_issue.description", {
- dns: this._dnsPrimaryInterfaceNameservers || "?",
+ dns: dnsPrimaryInterfaceNameservers || "?",
})}
${this.localize("network_issue.resolve_different")}
- ${!this._dnsPrimaryInterfaceNameservers
+ ${!dnsPrimaryInterfaceNameservers
? html`
${this.localize("network_issue.no_primary_interface")}
@@ -74,7 +68,7 @@ class LandingPageNetwork extends LitElement {
({ translationKey }, key) =>
html`${this.localize(translationKey)}`
@@ -84,97 +78,40 @@ class LandingPageNetwork extends LitElement {
`;
}
- protected firstUpdated(_changedProperties: PropertyValues): void {
- super.firstUpdated(_changedProperties);
- this._pingSupervisor();
- }
+ private _getPrimaryInterface = memoizeOne((interfaces?: NetworkInterface[]) =>
+ interfaces?.find((intf) => intf.primary && intf.enabled)
+ );
- private _schedulePingSupervisor() {
- setTimeout(
- () => this._pingSupervisor(),
- SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
- );
- }
-
- private async _pingSupervisor() {
- try {
- const response = await pingSupervisor();
- if (!response.ok) {
- throw new Error("Failed to ping supervisor, assume update in progress");
- }
- this._fetchSupervisorInfo();
- } catch (err) {
- // eslint-disable-next-line no-console
- console.error(err);
- this._schedulePingSupervisor();
- }
- }
-
- private _scheduleFetchSupervisorInfo() {
- setTimeout(
- () => this._fetchSupervisorInfo(),
- SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
- );
- }
-
- private async _fetchSupervisorInfo() {
- let data;
- try {
- const response = await getSupervisorNetworkInfo();
- if (!response.ok) {
- throw new Error("Failed to fetch network info");
- }
-
- ({ data } = await response.json());
- } catch (err) {
- // eslint-disable-next-line no-console
- console.error(err);
- this._getNetworkInfoError = true;
- this._dnsPrimaryInterfaceNameservers = undefined;
- this._dnsPrimaryInterface = undefined;
- return;
- }
-
- this._getNetworkInfoError = false;
-
- const primaryInterface = data.interfaces.find(
- (intf) => intf.primary && intf.enabled
- );
- if (primaryInterface) {
- this._dnsPrimaryInterfaceNameservers = [
+ private _getPrimaryNameservers = memoizeOne(
+ (primaryInterface: NetworkInterface) =>
+ [
...(primaryInterface.ipv4?.nameservers || []),
...(primaryInterface.ipv6?.nameservers || []),
- ].join(", ");
-
- this._dnsPrimaryInterface = primaryInterface.interface;
- } else {
- this._dnsPrimaryInterfaceNameservers = undefined;
- this._dnsPrimaryInterface = undefined;
- }
-
- if (!data.host_internet) {
- this._networkIssue = true;
- } else {
- this._networkIssue = false;
- }
-
- fireEvent(this, "value-changed", {
- value: this._networkIssue,
- });
- this._scheduleFetchSupervisorInfo();
- }
+ ].join(", ")
+ );
private async _setDns(ev) {
+ const primaryInterface = this._getPrimaryInterface(
+ this.networkInfo?.interfaces
+ );
+
const index = ev.target?.index;
try {
+ const dnsPrimaryInterface = primaryInterface?.interface;
+ if (!dnsPrimaryInterface) {
+ throw new Error("No primary interface found");
+ }
+
const response = await setSupervisorNetworkDns(
index,
- this._dnsPrimaryInterface!
+ dnsPrimaryInterface
);
if (!response.ok) {
throw new Error("Failed to set DNS");
}
- this._networkIssue = false;
+
+ // notify landing page to trigger a network info reload
+ fireEvent(this, "dns-set");
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);
@@ -205,4 +142,7 @@ declare global {
interface HTMLElementTagNameMap {
"landing-page-network": LandingPageNetwork;
}
+ interface HASSDomEvents {
+ "dns-set": undefined;
+ }
}
diff --git a/landing-page/src/data/supervisor.ts b/landing-page/src/data/supervisor.ts
index 41c28297e0..1f0fcff24d 100644
--- a/landing-page/src/data/supervisor.ts
+++ b/landing-page/src/data/supervisor.ts
@@ -1,4 +1,17 @@
import type { LandingPageKeys } from "../../../src/common/translations/localize";
+import type { HassioResponse } from "../../../src/data/hassio/common";
+import type {
+ DockerNetwork,
+ NetworkInterface,
+} from "../../../src/data/hassio/network";
+import { handleFetchPromise } from "../../../src/util/hass-call-api";
+
+export interface NetworkInfo {
+ interfaces: NetworkInterface[];
+ docker: DockerNetwork;
+ host_internet: boolean;
+ supervisor_internet: boolean;
+}
export const ALTERNATIVE_DNS_SERVERS: {
ipv4: string[];
@@ -37,8 +50,11 @@ export async function pingSupervisor() {
return fetch("/supervisor-api/supervisor/ping");
}
-export async function getSupervisorNetworkInfo() {
- return fetch("/supervisor-api/network/info");
+export async function getSupervisorNetworkInfo(): Promise {
+ const responseData = await handleFetchPromise>(
+ fetch("/supervisor-api/network/info")
+ );
+ return responseData?.data;
}
export const setSupervisorNetworkDns = async (
diff --git a/landing-page/src/ha-landing-page.ts b/landing-page/src/ha-landing-page.ts
index 83faa88675..e4c55d6311 100644
--- a/landing-page/src/ha-landing-page.ts
+++ b/landing-page/src/ha-landing-page.ts
@@ -10,36 +10,56 @@ import { extractSearchParam } from "../../src/common/url/search-params";
import { onBoardingStyles } from "../../src/onboarding/styles";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { LandingPageBaseElement } from "./landing-page-base-element";
+import {
+ getSupervisorNetworkInfo,
+ pingSupervisor,
+ type NetworkInfo,
+} from "./data/supervisor";
-const SCHEDULE_CORE_CHECK_SECONDS = 5;
+export const ASSUME_CORE_START_SECONDS = 30;
+const SCHEDULE_CORE_CHECK_SECONDS = 1;
+const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
@customElement("ha-landing-page")
class HaLandingPage extends LandingPageBaseElement {
@property({ attribute: false }) public translationFragment = "landing-page";
- @state() private _networkIssue = false;
-
@state() private _supervisorError = false;
+ @state() private _networkInfo?: NetworkInfo;
+
+ @state() private _coreStatusChecked = false;
+
+ @state() private _networkInfoError = false;
+
+ @state() private _coreCheckActive = false;
+
private _mobileApp =
extractSearchParam("redirect_uri") === "homeassistant://auth-callback";
render() {
+ const networkIssue = this._networkInfo && !this._networkInfo.host_internet;
+
return html`
${this.localize("header")}
- ${!this._networkIssue && !this._supervisorError
+ ${!networkIssue && !this._supervisorError
? html`
${this.localize("subheader")}
`
: nothing}
-
-
+ ${networkIssue || this._networkInfoError
+ ? html`
+
+ `
+ : nothing}
${this._supervisorError
? html`
this._checkCoreAvailability(),
- SCHEDULE_CORE_CHECK_SECONDS * 1000
+ () => this._fetchSupervisorInfo(true),
+ // on assumed core start check every second, otherwise every 5 seconds
+ (this._coreCheckActive
+ ? SCHEDULE_CORE_CHECK_SECONDS
+ : SCHEDULE_FETCH_NETWORK_INFO_SECONDS) * 1000
);
}
+ private _scheduleTurnOffCoreCheck() {
+ setTimeout(() => {
+ this._coreCheckActive = false;
+ }, ASSUME_CORE_START_SECONDS * 1000);
+ }
+
+ private async _fetchSupervisorInfo(schedule = false) {
+ try {
+ const response = await pingSupervisor();
+ if (!response.ok) {
+ throw new Error("ping-failed");
+ }
+
+ this._networkInfo = await getSupervisorNetworkInfo();
+ this._networkInfoError = false;
+ this._coreStatusChecked = false;
+ } catch (err: any) {
+ if (!this._coreStatusChecked) {
+ // wait before show errors, because we assume that core is starting
+ this._coreCheckActive = true;
+ this._scheduleTurnOffCoreCheck();
+ }
+ await this._checkCoreAvailability();
+
+ // assume supervisor update if ping fails -> don't show an error
+ if (!this._coreCheckActive && err.message !== "ping-failed") {
+ // eslint-disable-next-line no-console
+ console.error(err);
+ this._networkInfoError = true;
+ }
+ }
+
+ if (schedule) {
+ this._scheduleFetchSupervisorInfo();
+ }
+ }
+
private async _checkCoreAvailability() {
try {
const response = await fetch("/manifest.json");
if (response.ok) {
location.reload();
+ } else {
+ throw new Error("Failed to fetch manifest");
}
- } finally {
- this._scheduleCoreCheck();
+ } catch (_err) {
+ this._coreStatusChecked = true;
}
}
@@ -113,10 +175,6 @@ class HaLandingPage extends LandingPageBaseElement {
this._supervisorError = true;
}
- private _networkInfoChanged(ev: CustomEvent) {
- this._networkIssue = ev.detail.value;
- }
-
private _languageChanged(ev: CustomEvent) {
const language = ev.detail.value;
if (language !== this.language && language) {
diff --git a/src/common/util/wait.ts b/src/common/util/wait.ts
new file mode 100644
index 0000000000..7ed8544d66
--- /dev/null
+++ b/src/common/util/wait.ts
@@ -0,0 +1,6 @@
+export const waitForMs = (ms: number) =>
+ new Promise((resolve) => {
+ setTimeout(resolve, ms);
+ });
+
+export const waitForSeconds = (seconds: number) => waitForMs(seconds * 1000);
diff --git a/src/data/hassio/network.ts b/src/data/hassio/network.ts
index 0107e9b033..40500360af 100644
--- a/src/data/hassio/network.ts
+++ b/src/data/hassio/network.ts
@@ -21,7 +21,7 @@ export interface NetworkInterface {
wifi?: Partial | null;
}
-interface DockerNetwork {
+export interface DockerNetwork {
address: string;
dns: string;
gateway: string;
From 782df0473c7178218c2f8033f4cb2a0e9c9f19f0 Mon Sep 17 00:00:00 2001
From: Petar Petrov
Date: Wed, 5 Mar 2025 15:05:01 +0200
Subject: [PATCH 32/38] Fix height of chart legend (#24519)
---
src/components/chart/ha-chart-base.ts | 2 +-
src/panels/lovelace/cards/hui-history-graph-card.ts | 5 ++++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts
index b5656fad04..8bda2bfba8 100644
--- a/src/components/chart/ha-chart-base.ts
+++ b/src/components/chart/ha-chart-base.ts
@@ -701,7 +701,7 @@ export class HaChartBase extends LitElement {
.chart-legend {
max-height: 60%;
overflow-y: auto;
- margin: 12px 0 0;
+ padding: 12px 0 0;
font-size: 12px;
color: var(--primary-text-color);
}
diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts
index 8da9b69191..65847c5ad0 100644
--- a/src/panels/lovelace/cards/hui-history-graph-card.ts
+++ b/src/panels/lovelace/cards/hui-history-graph-card.ts
@@ -261,6 +261,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
class="content ${classMap({
"has-header": !!this._config.title,
"has-rows": !!this._config.grid_options?.rows,
+ "has-height": hasFixedHeight,
})}"
>
${this._error
@@ -320,9 +321,11 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
padding-top: 0;
}
state-history-charts {
- height: 100%;
--timeline-top-margin: 16px;
}
+ .has-height state-history-charts {
+ height: 100%;
+ }
.has-rows {
--chart-max-height: 100%;
}
From f132a32fd4dba89538f8a58ebac945427c8395f4 Mon Sep 17 00:00:00 2001
From: Petar Petrov
Date: Thu, 6 Mar 2025 12:30:17 +0200
Subject: [PATCH 33/38] Ignore excessive keydown events in charts (#24523)
* Ignore excessive keydown events in charts
* lint
---
src/components/chart/ha-chart-base.ts | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts
index 8bda2bfba8..bbf2fcacd7 100644
--- a/src/components/chart/ha-chart-base.ts
+++ b/src/components/chart/ha-chart-base.ts
@@ -108,7 +108,10 @@ export class HaChartBase extends LitElement {
// Add keyboard event listeners
const handleKeyDown = (ev: KeyboardEvent) => {
- if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
+ if (
+ !this._modifierPressed &&
+ ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
+ ) {
this._modifierPressed = true;
if (!this.options?.dataZoom) {
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
@@ -123,7 +126,10 @@ export class HaChartBase extends LitElement {
};
const handleKeyUp = (ev: KeyboardEvent) => {
- if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
+ if (
+ this._modifierPressed &&
+ ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
+ ) {
this._modifierPressed = false;
if (!this.options?.dataZoom) {
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
From 022ef982caa59195916f3daa94ac7deca57596bc Mon Sep 17 00:00:00 2001
From: Paul Bottein
Date: Thu, 6 Mar 2025 15:55:20 +0100
Subject: [PATCH 34/38] Only recreate stack editor when the type or index
change (#24530)
---
.../config-elements/hui-stack-card-editor.ts | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts
index 595eae1866..c08c959c22 100644
--- a/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts
@@ -85,7 +85,7 @@ export class HuiStackCardEditor
@state() protected _guiModeAvailable? = true;
- protected _keys = new WeakMap();
+ protected _keys = new Map();
protected _schema: readonly HaFormSchema[] = SCHEMA;
@@ -203,7 +203,7 @@ export class HuiStackCardEditor
>
${keyed(
- this._getKey(this._config.cards[selected]),
+ this._getKey(this._config.cards, selected),
html`
Date: Thu, 6 Mar 2025 15:57:06 +0100
Subject: [PATCH 35/38] Bumped version to 20250306.0
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 68228b8f2e..f88c997698 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
-version = "20250305.0"
+version = "20250306.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
From e871dc8151601109cbaa27af59f324b3b64f26ae Mon Sep 17 00:00:00 2001
From: Wendelin <12148533+wendevlin@users.noreply.github.com>
Date: Wed, 12 Mar 2025 16:19:42 +0100
Subject: [PATCH 36/38] Increase core start seconds (#24604)
---
landing-page/src/ha-landing-page.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/landing-page/src/ha-landing-page.ts b/landing-page/src/ha-landing-page.ts
index e4c55d6311..06d1c43716 100644
--- a/landing-page/src/ha-landing-page.ts
+++ b/landing-page/src/ha-landing-page.ts
@@ -16,7 +16,7 @@ import {
type NetworkInfo,
} from "./data/supervisor";
-export const ASSUME_CORE_START_SECONDS = 30;
+export const ASSUME_CORE_START_SECONDS = 60;
const SCHEDULE_CORE_CHECK_SECONDS = 1;
const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
From df934cfed957748633836b013fee9e1e9a1b60a3 Mon Sep 17 00:00:00 2001
From: karwosts <32912880+karwosts@users.noreply.github.com>
Date: Wed, 12 Mar 2025 10:06:31 -0700
Subject: [PATCH 37/38] Energy self sufficiency gauge needs grid consumption
(#24606)
---
src/panels/energy/strategies/energy-view-strategy.ts | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/src/panels/energy/strategies/energy-view-strategy.ts b/src/panels/energy/strategies/energy-view-strategy.ts
index 92d61c435e..808d4504b0 100644
--- a/src/panels/energy/strategies/energy-view-strategy.ts
+++ b/src/panels/energy/strategies/energy-view-strategy.ts
@@ -148,11 +148,13 @@ export class EnergyViewStrategy extends ReactiveElement {
collection_key: "energy_dashboard",
});
}
- view.cards!.push({
- type: "energy-self-sufficiency-gauge",
- view_layout: { position: "sidebar" },
- collection_key: "energy_dashboard",
- });
+ if (hasGrid) {
+ view.cards!.push({
+ type: "energy-self-sufficiency-gauge",
+ view_layout: { position: "sidebar" },
+ collection_key: "energy_dashboard",
+ });
+ }
}
// Only include if we have a grid
From 46c9af75fde7d8507fca28ac03c57def0417fc05 Mon Sep 17 00:00:00 2001
From: Bram Kragten
Date: Wed, 12 Mar 2025 18:53:49 +0100
Subject: [PATCH 38/38] Bumped version to 20250312.0
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index f88c997698..a1049bd9ad 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
-version = "20250306.0"
+version = "20250312.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"