Compare commits

..

3 Commits

Author SHA1 Message Date
Wendelin df561d3f91 reduce to 2 seconds 2026-06-02 15:44:11 +02:00
Wendelin f5f534ccbe add core download progress 2026-06-02 15:13:34 +02:00
Wendelin 32a08a3ad2 Simplify and improve landingpage 2026-06-02 13:35:26 +02:00
39 changed files with 331 additions and 370 deletions
@@ -13,7 +13,7 @@ Our dialogs are based on the latest version of Material Design. Please note that
- Dialogs have a max width of 560px. Alert and confirmation dialogs have a fixed width of 320px. If you need more width, consider a dedicated page instead.
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guidelines.
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user **has made changes to**. Instead it will animate "no" by a little shake.
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake.
- Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu.
- The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom.
- Keep the labels short, for example `Save`, `Delete`, `Enable`.
+31
View File
@@ -13,6 +13,28 @@ export interface NetworkInfo {
supervisor_internet: boolean;
}
interface SupervisorJob {
name: string;
reference: string | null;
uuid: string;
progress: number; // float, 0100
stage: string | null;
done: boolean;
errors: {
type: string;
message: string;
stage: string | null;
}[];
created: string; // ISO datetime string
extra: Record<string, unknown> | null;
child_jobs: SupervisorJob[];
}
export interface SupervisorJobInfo {
ignore_conditions: string[];
jobs: SupervisorJob[];
}
export const ALTERNATIVE_DNS_SERVERS: {
ipv4: string[];
ipv6: string[];
@@ -57,6 +79,15 @@ export async function getSupervisorNetworkInfo(): Promise<NetworkInfo> {
return responseData?.data;
}
export async function getSupervisorJobsInfo(): Promise<
HassioResponse<SupervisorJobInfo>
> {
const responseData = await handleFetchPromise<
HassioResponse<SupervisorJobInfo>
>(fetch("/supervisor-api/jobs/info"));
return responseData;
}
export const setSupervisorNetworkDns = async (
dnsServerIndex: number,
primaryInterface: string
+56 -6
View File
@@ -2,9 +2,9 @@ import { mdiOpenInNew } from "@mdi/js";
import { css, html, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { extractSearchParam } from "../../src/common/url/search-params";
import "../../src/components/animation/ha-fade-in";
import "../../src/components/ha-alert";
import "../../src/components/ha-button";
import "../../src/components/animation/ha-fade-in";
import "../../src/components/ha-spinner";
import "../../src/components/ha-svg-icon";
import "../../src/components/progress/ha-progress-bar";
@@ -15,6 +15,7 @@ import { haStyle } from "../../src/resources/styles";
import "./components/landing-page-logs";
import "./components/landing-page-network";
import {
getSupervisorJobsInfo,
getSupervisorNetworkInfo,
pingSupervisor,
type NetworkInfo,
@@ -24,6 +25,7 @@ import { LandingPageBaseElement } from "./landing-page-base-element";
export const ASSUME_CORE_START_SECONDS = 60;
const SCHEDULE_CORE_CHECK_SECONDS = 1;
const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
const SCHEDULE_FETCH_JOBS_INFO_SECONDS = 2;
@customElement("ha-landing-page")
class HaLandingPage extends LandingPageBaseElement {
@@ -39,6 +41,8 @@ class HaLandingPage extends LandingPageBaseElement {
@state() private _coreCheckActive = false;
@state() private _progress = -1;
private _mobileApp =
extractSearchParam("redirect_uri") === "homeassistant://auth-callback";
@@ -60,7 +64,14 @@ class HaLandingPage extends LandingPageBaseElement {
${!networkIssue && !this._supervisorError
? html`
<p>${this.localize("subheader")}</p>
<ha-progress-bar indeterminate></ha-progress-bar>
<ha-progress-bar
.indeterminate=${this._progress <= 0}
.value=${this._progress > 0 ? this._progress : undefined}
.loading=${this._progress >= 0}
>${this._progress >= 0
? `${this._progress}%`
: nothing}</ha-progress-bar
>
`
: nothing}
${networkIssue || this._networkInfoError
@@ -126,6 +137,7 @@ class HaLandingPage extends LandingPageBaseElement {
import("../../src/components/ha-language-picker");
this._fetchSupervisorInfo(true);
this._fetchSupervisorJobsInfo();
}
private _scheduleFetchSupervisorInfo() {
@@ -138,6 +150,13 @@ class HaLandingPage extends LandingPageBaseElement {
);
}
private _scheduleFetchSupervisorJobsInfo() {
setTimeout(
() => this._fetchSupervisorJobsInfo(),
SCHEDULE_FETCH_JOBS_INFO_SECONDS * 1000
);
}
private _scheduleTurnOffCoreCheck() {
setTimeout(() => {
this._coreCheckActive = false;
@@ -165,7 +184,7 @@ class HaLandingPage extends LandingPageBaseElement {
// 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);
console.error("Failed to fetch supervisor info", err);
this._networkInfoError = true;
}
}
@@ -175,6 +194,31 @@ class HaLandingPage extends LandingPageBaseElement {
}
}
private async _fetchSupervisorJobsInfo() {
try {
const jobsInfo = await getSupervisorJobsInfo();
if (
jobsInfo.result === "ok" &&
jobsInfo.data.jobs.length &&
jobsInfo.data.jobs[0].name === "home_assistant_core_install"
) {
this._progress = jobsInfo.data.jobs[0].progress;
} else {
this._progress = -1;
}
} catch (err: any) {
await this._checkCoreAvailability();
if (!this._coreCheckActive) {
this._progress = -1;
// eslint-disable-next-line no-console
console.error("Failed to fetch supervisor jobs info", err);
}
}
this._scheduleFetchSupervisorJobsInfo();
}
private async _checkCoreAvailability() {
try {
const response = await fetch("/manifest.json");
@@ -222,21 +266,27 @@ class HaLandingPage extends LandingPageBaseElement {
flex-direction: column;
gap: var(--ha-space-4);
}
ha-language-picker {
min-width: 200px;
}
ha-alert p {
text-align: unset;
}
.footer ha-svg-icon {
--mdc-icon-size: var(--ha-space-5);
}
ha-language-picker {
margin-inline-start: calc(-1 * var(--ha-space-4));
}
ha-button {
margin-inline-end: calc(-1 * var(--ha-space-2));
}
ha-fade-in {
min-height: calc(100vh - 64px - 88px);
display: flex;
justify-content: center;
align-items: center;
}
ha-progress-bar {
--ha-progress-bar-track-height: 20px;
}
`,
];
}
+1 -1
View File
@@ -180,7 +180,7 @@
"jsdom": "29.1.1",
"jszip": "3.10.1",
"license-checker-rseidelsohn": "5.0.1",
"lint-staged": "17.0.6",
"lint-staged": "17.0.5",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.18.1",
@@ -1,5 +1,6 @@
import { LitElement, css, html } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../ha-tooltip";
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
@@ -12,6 +13,7 @@ export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
*
* @attr {"pass"|"fail"|"invalid"|"unknown"} state - The current live-test state. Defaults to `unknown`.
* @attr {string} label - Accessible label announced by assistive technology.
* @attr {string} message - Optional tooltip body shown on hover/focus.
*/
@customElement("ha-automation-row-live-test")
export class HaAutomationRowLiveTest extends LitElement {
@@ -19,6 +21,8 @@ export class HaAutomationRowLiveTest extends LitElement {
@property() public label = "";
@property() public message?: string;
protected render() {
return html`
<div
@@ -27,38 +31,39 @@ export class HaAutomationRowLiveTest extends LitElement {
tabindex="0"
aria-label=${this.label}
></div>
${this.message
? html`<ha-tooltip for="indicator">${this.message}</ha-tooltip>`
: nothing}
`;
}
static styles = css`
:host {
position: absolute;
top: -5px;
inset-inline-end: -6px;
display: inline-block;
}
#indicator {
width: 10px;
height: 10px;
width: 12px;
height: 12px;
border-radius: var(--ha-border-radius-circle);
border: var(--ha-border-width-md) solid;
border: 3px solid;
box-sizing: border-box;
background-color: var(--card-background-color);
box-shadow: 0 0 0 2px var(--card-background-color);
transition: all var(--ha-animation-duration-normal) ease-in-out;
}
:host([state="pass"]) #indicator {
background-color: var(--ha-color-green-60);
border-color: var(--ha-color-green-60);
background-color: var(--ha-color-fill-success-loud-resting);
border-color: var(--ha-color-fill-success-loud-resting);
}
:host([state="fail"]) #indicator {
border-color: var(--ha-color-orange-60);
border-color: var(--ha-color-fill-warning-loud-resting);
}
:host([state="invalid"]) #indicator {
border-color: var(--ha-color-red-60);
border-color: var(--ha-color-fill-danger-loud-resting);
}
:host([state="unknown"]) #indicator {
border-color: var(--ha-color-neutral-60);
border-color: var(--ha-color-fill-neutral-loud-resting);
}
`;
}
@@ -165,7 +165,7 @@ export class HaAutomationRow extends LitElement {
::slotted([slot="leading-icon"]) {
color: var(--ha-color-on-neutral-quiet);
}
:host([building-block]) ::slotted(#condition-icon) {
:host([building-block]) ::slotted([slot="leading-icon"]) {
--mdc-icon-size: var(--ha-space-5);
color: var(--white-color);
transform: rotate(-45deg);
+2 -6
View File
@@ -101,22 +101,18 @@ export class HaSankeyChart extends LitElement {
const value = this.valueFormatter
? this.valueFormatter(data.value)
: data.value;
// Keep numbers and units left-to-right, even in RTL locales.
const formattedValue = html`<div style="direction:ltr; display: inline;">
${value}
</div>`;
if (data.id) {
const node = this.data.nodes.find((n) => n.id === data.id);
return html`<ha-chart-tooltip-marker
.color=${String(params.color ?? "")}
></ha-chart-tooltip-marker>
${node?.label ?? data.id}<br />${formattedValue}`;
${node?.label ?? data.id}<br />${value}`;
}
if (data.source && data.target) {
const source = this.data.nodes.find((n) => n.id === data.source);
const target = this.data.nodes.find((n) => n.id === data.target);
return html`${source?.label ?? data.source}
${target?.label ?? data.target}<br />${formattedValue}`;
${target?.label ?? data.target}<br />${value}`;
}
return null;
};
-11
View File
@@ -485,17 +485,6 @@ export const migrateAutomationTrigger = (
}
delete trigger.platform;
}
if ("options" in trigger) {
if (trigger.options && "behavior" in trigger.options) {
if (trigger.options.behavior === "any") {
trigger.options.behavior = "each";
} else if (trigger.options.behavior === "last") {
trigger.options.behavior = "all";
}
}
}
return trigger;
};
+1 -16
View File
@@ -222,12 +222,6 @@ export interface EnergyPreferences {
device_consumption_water: DeviceConsumptionEnergyPreference[];
}
export const EMPTY_PREFERENCES: EnergyPreferences = {
energy_sources: [],
device_consumption: [],
device_consumption_water: [],
};
export interface EnergyInfo {
cost_sensors: Record<string, string>;
solar_forecast_domains: string[];
@@ -808,16 +802,7 @@ export const getEnergyDataCollection = (
if (!collection.prefs) {
// This will raise if not found.
// Detect by checking `e.code === "not_found"
try {
collection.prefs = await getEnergyPreferences(hass);
} catch (err: any) {
if (err.code === "not_found") {
return {
prefs: EMPTY_PREFERENCES,
} as EnergyData;
}
throw err;
}
collection.prefs = await getEnergyPreferences(hass);
}
scheduleHourlyRefresh(collection);
@@ -391,6 +391,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.localizeFunc}
.narrow=${this.narrow}
.isWide=${this.isWide}
.backPath=${this.backPath}
.backCallback=${this.backCallback}
+8 -14
View File
@@ -1,4 +1,3 @@
import { consume } from "@lit/context";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import {
@@ -19,7 +18,6 @@ import "../components/ha-icon-button-arrow-prev";
import "../components/ha-menu-button";
import "../components/ha-svg-icon";
import "../components/ha-tab";
import { narrowViewportContext } from "../data/context";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, Route } from "../types";
@@ -61,9 +59,7 @@ export class HassTabsSubpage extends LitElement {
@property({ attribute: false }) public tabs!: PageNavigation[];
@state()
@consume({ context: narrowViewportContext, subscribe: true })
private _narrow = false;
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean, reflect: true, attribute: "is-wide" })
public isWide = false;
@@ -120,7 +116,7 @@ export class HassTabsSubpage extends LitElement {
<a href=${page.path} @click=${this._tabClicked}>
<ha-tab
.active=${page.path === activeTab?.path}
.narrow=${this._narrow}
.narrow=${this.narrow}
.name=${page.translationKey
? localizeFunc(page.translationKey)
: page.name}
@@ -139,8 +135,6 @@ export class HassTabsSubpage extends LitElement {
);
public willUpdate(changedProperties: PropertyValues<this>) {
this.toggleAttribute("narrow", this._narrow);
if (changedProperties.has("route")) {
const currentPath = `${this.route.prefix}${this.route.path}`;
this._activeTab = this.tabs.find((tab) =>
@@ -157,18 +151,18 @@ export class HassTabsSubpage extends LitElement {
this.hass.config.components,
this.hass.language,
this.hass.userData,
this._narrow,
this.narrow,
this.localizeFunc || this.hass.localize
);
return html`
<div class="toolbar ${classMap({ narrow: this._narrow })}">
<div class="toolbar ${classMap({ narrow: this.narrow })}">
<slot name="toolbar">
<div class="toolbar-content">
${this.mainPage || (!this.backPath && history.state?.root)
? html`
<ha-menu-button
.hass=${this.hass}
.narrow=${this._narrow}
.narrow=${this.narrow}
></ha-menu-button>
`
: this.backPath
@@ -184,12 +178,12 @@ export class HassTabsSubpage extends LitElement {
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`}
${this._narrow || !this.showTabs
${this.narrow || !this.showTabs
? html`<div class="main-title">
<slot name="header">${!this.showTabs ? tabs[0] : ""}</slot>
</div>`
: ""}
${this.showTabs && !this._narrow
${this.showTabs && !this.narrow
? html`<div id="tabbar">${tabs}</div>`
: ""}
<div id="toolbar-icon">
@@ -197,7 +191,7 @@ export class HassTabsSubpage extends LitElement {
</div>
</div>
</slot>
${this.showTabs && this._narrow
${this.showTabs && this.narrow
? html`<div id="tabbar" class="bottom-bar">${tabs}</div>`
: ""}
</div>
+19 -30
View File
@@ -1,39 +1,20 @@
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { LocalizeFunc } from "../../common/translations/localize";
import { fireEvent } from "../../common/dom/fire_event";
import { customElement } from "lit/decorators";
import "../../components/ha-dialog";
import { DialogMixin } from "../../dialogs/dialog-mixin";
import type { AppDialogParams } from "./show-app-dialog";
@customElement("app-dialog")
class DialogApp extends LitElement {
@property({ attribute: false }) public localize?: LocalizeFunc;
@state() private _open = false;
public async showDialog(params: { localize: LocalizeFunc }): Promise<void> {
this.localize = params.localize;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this.localize = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
class DialogApp extends DialogMixin<AppDialogParams>(LitElement) {
protected render() {
if (!this.localize) {
if (!this.params?.localize) {
return nothing;
}
return html`<ha-dialog
.open=${this._open}
header-title=${this.localize(
open
header-title=${this.params.localize(
"ui.panel.page-onboarding.welcome.download_app"
) || "Click here to download the app"}
@closed=${this._dialogClosed}
>
<div>
<div class="app-qr">
@@ -45,13 +26,17 @@ class DialogApp extends LitElement {
<img
loading="lazy"
src="/static/images/appstore.svg"
alt=${this.localize("ui.panel.page-onboarding.welcome.appstore")}
alt=${this.params.localize(
"ui.panel.page-onboarding.welcome.appstore"
)}
class="icon"
/>
<img
loading="lazy"
src="/static/images/qr-appstore.svg"
alt=${this.localize("ui.panel.page-onboarding.welcome.appstore")}
alt=${this.params.localize(
"ui.panel.page-onboarding.welcome.appstore"
)}
/>
</a>
<a
@@ -62,13 +47,17 @@ class DialogApp extends LitElement {
<img
loading="lazy"
src="/static/images/playstore.svg"
alt=${this.localize("ui.panel.page-onboarding.welcome.playstore")}
alt=${this.params.localize(
"ui.panel.page-onboarding.welcome.playstore"
)}
class="icon"
/>
<img
loading="lazy"
src="/static/images/qr-playstore.svg"
alt=${this.localize("ui.panel.page-onboarding.welcome.playstore")}
alt=${this.params.localize(
"ui.panel.page-onboarding.welcome.playstore"
)}
/>
</a>
</div>
+59 -75
View File
@@ -1,103 +1,87 @@
import { mdiAccountGroup, mdiOpenInNew } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { LocalizeFunc } from "../../common/translations/localize";
import { customElement } from "lit/decorators";
import "../../components/ha-dialog";
import "../../components/ha-list";
import "../../components/ha-list-item";
import "../../components/ha-svg-icon";
import "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-nav";
import { DialogMixin } from "../../dialogs/dialog-mixin";
import type { CommunityDialogParams } from "./show-community-dialog";
@customElement("community-dialog")
class DialogCommunity extends LitElement {
@property({ attribute: false }) public localize?: LocalizeFunc;
@state() private _open = false;
public async showDialog(params): Promise<void> {
this.localize = params.localize;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this.localize = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
class DialogCommunity extends DialogMixin<CommunityDialogParams>(LitElement) {
protected render() {
if (!this.localize) {
if (!this.params?.localize) {
return nothing;
}
return html`<ha-dialog
.open=${this._open}
header-title=${this.localize(
open
header-title=${this.params.localize(
"ui.panel.page-onboarding.welcome.community"
)}
@closed=${this._dialogClosed}
>
<ha-list>
<a
<ha-list-nav>
<ha-list-item-button
target="_blank"
rel="noreferrer noopener"
href="https://community.home-assistant.io/"
>
<ha-list-item hasMeta graphic="icon">
<img
src="/static/icons/favicon-192x192.png"
slot="graphic"
alt="Home Assistant Logo"
/>
${this.localize("ui.panel.page-onboarding.welcome.forums")}
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item>
</a>
<a
<img
src="/static/icons/favicon-192x192.png"
slot="start"
alt="Home Assistant Logo"
/>
<span slot="headline">
${this.params.localize("ui.panel.page-onboarding.welcome.forums")}
</span>
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item-button>
<ha-list-item-button
target="_blank"
rel="noreferrer noopener"
href="https://newsletter.openhomefoundation.org/"
>
<ha-list-item hasMeta graphic="icon">
<img
src="/static/icons/logo_ohf.svg"
slot="graphic"
alt="Open Home Foundation Logo"
/>
${this.localize(
<img
src="/static/icons/logo_ohf.svg"
slot="start"
alt="Open Home Foundation Logo"
/>
<span slot="headline">
${this.params.localize(
"ui.panel.page-onboarding.welcome.open_home_newsletter"
)}
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item>
</a>
<a
</span>
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item-button>
<ha-list-item-button
target="_blank"
rel="noreferrer noopener"
href="https://www.home-assistant.io/join-chat"
>
<ha-list-item hasMeta graphic="icon">
<img
src="/static/images/logo_discord.png"
slot="graphic"
alt="Discord Logo"
/>
${this.localize("ui.panel.page-onboarding.welcome.discord")}
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item>
</a>
<a
<img
src="/static/images/logo_discord.png"
slot="start"
alt="Discord Logo"
/>
<span slot="headline">
${this.params.localize("ui.panel.page-onboarding.welcome.discord")}
</span>
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item-button>
<ha-list-item-button
target="_blank"
rel="noreferrer noopener"
href="https://fosstodon.org/@homeassistant"
>
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon .path=${mdiAccountGroup} slot="graphic"></ha-svg-icon>
${this.localize("ui.panel.page-onboarding.welcome.social_media")}
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item>
</a>
</ha-list>
<ha-svg-icon .path=${mdiAccountGroup} slot="start"></ha-svg-icon>
<span slot="headline">
${this.params.localize(
"ui.panel.page-onboarding.welcome.social_media"
)}
</span>
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item-button>
</ha-list-nav>
</ha-dialog>`;
}
@@ -105,12 +89,12 @@ class DialogCommunity extends LitElement {
ha-dialog {
--dialog-content-padding: 0;
}
ha-list-item {
height: 56px;
--mdc-list-item-meta-size: 20px;
img {
width: 32px;
height: 32px;
}
a {
text-decoration: none;
ha-svg-icon {
color: var(--ha-color-text-secondary);
}
`;
}
+6 -1
View File
@@ -3,13 +3,18 @@ import type { LocalizeFunc } from "../../common/translations/localize";
export const loadAppDialog = () => import("./app-dialog");
export interface AppDialogParams {
localize: LocalizeFunc;
}
export const showAppDialog = (
element: HTMLElement,
params: { localize: LocalizeFunc }
params: AppDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "app-dialog",
dialogImport: loadAppDialog,
dialogParams: params,
addHistory: false,
});
};
@@ -3,13 +3,18 @@ import type { LocalizeFunc } from "../../common/translations/localize";
export const loadCommunityDialog = () => import("./community-dialog");
export interface CommunityDialogParams {
localize: LocalizeFunc;
}
export const showCommunityDialog = (
element: HTMLElement,
params: { localize: LocalizeFunc }
params: CommunityDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "community-dialog",
dialogImport: loadCommunityDialog,
dialogParams: params,
addHistory: false,
});
};
@@ -1,5 +1,5 @@
import "@home-assistant/webawesome/dist/components/tag/tag";
import { mdiCheckCircle, mdiHelpCircleOutline } from "@mdi/js";
import { mdiHelpCircleOutline } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
@@ -25,9 +25,7 @@ class SupervisorAppsCardContent extends LitElement {
@property() public stage: AddonStage = "stable";
@property() public state?: AddonState;
@property({ type: Boolean }) public installed = false;
@property() public state: AddonState = null;
@property() public description?: string;
@@ -79,23 +77,13 @@ class SupervisorAppsCardContent extends LitElement {
</div>
</div>
</div>
${this.tags?.length || this.state !== undefined || this.installed
${this.tags?.length || this.state
? html`
<div class="footer">
${this.state !== undefined
? html`<supervisor-apps-state
.state=${this.state || "unknown"}
></supervisor-apps-state>`
: this.installed
? html`<div class="installed">
<ha-svg-icon .path=${mdiCheckCircle}></ha-svg-icon>
<span
>${this.hass.localize(
"ui.panel.config.apps.state.installed"
)}</span
>
</div>`
: html`<span></span>`}
<supervisor-apps-state
.state=${this.state || "unknown"}
></supervisor-apps-state>
${this.tags?.length
? html`<div class="tags">
${this.tags.map(
@@ -171,17 +159,6 @@ class SupervisorAppsCardContent extends LitElement {
display: flex;
gap: var(--ha-space-2);
}
.installed {
display: inline-flex;
align-items: center;
gap: var(--ha-space-2);
color: var(--ha-color-text-secondary);
font-size: var(--ha-font-size-m);
}
.installed ha-svg-icon {
--mdc-icon-size: 16px;
color: var(--ha-color-on-success-normal);
}
`;
}
@@ -135,6 +135,7 @@ class HaConfigAppDashboard extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${route}
.tabs=${addonTabs}
back-path=${this._fromStore ? "/config/apps/available" : "/config/apps"}
@@ -1,14 +1,7 @@
import {
mdiAlertDecagramOutline,
mdiArrowUpBoldCircle,
mdiArrowUpBoldCircleOutline,
mdiFlask,
mdiPuzzle,
} from "@mdi/js";
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
@@ -17,7 +10,6 @@ import type { HassioAddonRepository } from "../../../data/hassio/addon";
import type { StoreAddon } from "../../../data/supervisor/store";
import type { HomeAssistant } from "../../../types";
import "./components/supervisor-apps-card-content";
import type { AppTag } from "./components/supervisor-apps-card-content";
import { filterAndSort } from "./components/supervisor-apps-filter";
import { supervisorAppsStyle } from "./resources/supervisor-apps-style";
@@ -62,29 +54,21 @@ export class SupervisorAppsRepositoryEl extends LitElement {
<div class="content">
<h1>${repo.name}</h1>
<div class="card-group">
${addons.map((addon) => {
const tags = this._getAppTags(addon);
return html`
${addons.map(
(addon) => html`
<ha-card
outlined
.addon=${addon}
class=${addon.available ? "" : "not_available"}
@click=${this._addonTapped}
>
<div
class=${classMap({
"card-content": true,
"has-footer": tags.length > 0 || addon.installed,
})}
>
<div class="card-content">
<supervisor-apps-card-content
.hass=${this.hass}
.title=${addon.name}
.stage=${addon.stage}
.description=${addon.description}
.available=${addon.available}
.installed=${addon.installed}
.tags=${tags}
.icon=${addon.installed && addon.update_available
? mdiArrowUpBoldCircle
: mdiPuzzle}
@@ -124,8 +108,8 @@ export class SupervisorAppsRepositoryEl extends LitElement {
></supervisor-apps-card-content>
</div>
</ha-card>
`;
})}
`
)}
</div>
</div>
`;
@@ -135,32 +119,6 @@ export class SupervisorAppsRepositoryEl extends LitElement {
navigate(`/config/app/${ev.currentTarget.addon.slug}/info?store=true`);
}
private _getAppTags(addon: StoreAddon): AppTag[] {
const labels: AppTag[] = [];
if (addon.installed && addon.update_available) {
labels.push({
label: this.hass.localize(
`ui.panel.config.apps.state.update_available`
),
variant: "brand",
iconPath: mdiArrowUpBoldCircleOutline,
});
}
if (addon.stage !== "stable") {
labels.push({
label: this.hass.localize(
`ui.panel.config.apps.dashboard.capability.stages.${addon.stage}`
),
variant: addon.stage === "experimental" ? "warning" : "danger",
iconPath:
addon.stage === "experimental" ? mdiFlask : mdiAlertDecagramOutline,
});
}
return labels;
}
static get styles(): CSSResultGroup {
return [
supervisorAppsStyle,
@@ -169,9 +127,6 @@ export class SupervisorAppsRepositoryEl extends LitElement {
cursor: pointer;
overflow: hidden;
}
.card-content.has-footer {
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
}
.not_available {
opacity: 0.6;
}
@@ -82,6 +82,8 @@ export class HaConfigAreasDashboard extends LitElement {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _hierarchy?: AreasFloorHierarchy;
@@ -167,6 +169,7 @@ export class HaConfigAreasDashboard extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.backPath=${this._searchParms.has("historyBack")
? undefined
@@ -52,7 +52,6 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-tooltip";
import type {
AutomationClipboard,
Condition,
@@ -212,27 +211,11 @@ export default class HaAutomationConditionRow extends LitElement {
);
return html`
<div id="condition-icon" class="icon-badge-wrapper" slot="leading-icon">
<ha-condition-icon
.hass=${this.hass}
.condition=${this.condition.condition}
></ha-condition-icon>
${this.optionsInSidebar && this.condition.condition !== "trigger"
? html`<ha-automation-row-live-test
.state=${this._liveTestResult.state}
.label=${this.hass.localize(
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult.state}`
)}
></ha-automation-row-live-test>`
: nothing}
</div>
${this.optionsInSidebar &&
this.condition.condition !== "trigger" &&
this._liveTestResult.message
? html`<ha-tooltip for="condition-icon" slot="leading-icon"
>${this._liveTestResult.message}</ha-tooltip
>`
: nothing}
<ha-condition-icon
slot="leading-icon"
.hass=${this.hass}
.condition=${this.condition.condition}
></ha-condition-icon>
<h3 slot="header">
${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
@@ -548,7 +531,17 @@ export default class HaAutomationConditionRow extends LitElement {
@click=${this._toggleSidebar}
@toggle-collapsed=${this._toggleCollapse}
>${this._renderRow()}
</ha-automation-row>`
<ha-automation-row-live-test
slot="icons"
.state=${this.condition.condition !== "trigger"
? this._liveTestResult.state
: "unknown"}
.label=${this.hass.localize(
`ui.panel.config.automation.editor.conditions.live_test_state.${this.condition.condition !== "trigger" ? this._liveTestResult.state : "unknown"}`
)}
.message=${this._liveTestResult.message}
></ha-automation-row-live-test
></ha-automation-row>`
: html`
<ha-expansion-panel
left-chevron
-5
View File
@@ -53,11 +53,6 @@ export const rowStyles = css`
position: absolute;
}
.icon-badge-wrapper {
position: relative;
display: inline-flex;
}
.note-indicator {
color: var(--ha-color-on-neutral-normal);
}
@@ -118,6 +118,7 @@ class HaConfigEnergy extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config/lovelace/dashboards"}
@@ -58,6 +58,8 @@ const DATA_SET_CONFIG: SeriesOption = {
class HaConfigHardwareOverview extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _error?: string;
@@ -247,6 +249,7 @@ class HaConfigHardwareOverview extends SubscribeMixin(LitElement) {
<hass-tabs-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${hardwareTabs(this.hass)}
>
@@ -511,6 +511,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${this._searchParams.has("historyBack")
? undefined
: "/config"}
@@ -25,7 +25,6 @@ import {
type ExtEntityRegistryEntry,
} from "../../../../../data/entity/entity_registry";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { OVERRIDE_DEVICE_CLASSES } from "../../../entities/entity-registry-settings-editor";
import "./matter-add-device/matter-add-device-apple-home";
import "./matter-add-device/matter-add-device-existing";
import "./matter-add-device/matter-add-device-generic";
@@ -140,17 +139,15 @@ class DialogMatterAddDevice extends LitElement {
entityIds
);
this._mainEntity = Object.values(entries).find((entry) => {
if (entry.entity_category) return false;
const domain = computeDomain(entry.entity_id);
const deviceClasses = OVERRIDE_DEVICE_CLASSES[domain];
if (!deviceClasses) return false;
const deviceClass = entry.device_class ?? entry.original_device_class;
if (!deviceClass) return false;
return deviceClasses.some(
(classes) => classes.length > 1 && classes.includes(deviceClass)
);
});
const mainEntry = Object.values(entries).find(
(e) => e.original_name === null
);
if (!mainEntry) return;
const domain = computeDomain(mainEntry.entity_id);
if (domain === "cover" || domain === "binary_sensor") {
this._mainEntity = mainEntry;
}
}
private _dialogClosed(): void {
@@ -39,6 +39,8 @@ export class HaConfigPerson extends LitElement {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _storageItems?: Person[];
@@ -59,6 +61,7 @@ export class HaConfigPerson extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config"
.tabs=${configSections.persons}
@@ -27,6 +27,8 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
protected render() {
@@ -37,6 +39,7 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${voiceAssistantTabs}
+1
View File
@@ -234,6 +234,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.backPath=${this._searchParms.has("historyBack")
? undefined
+1 -10
View File
@@ -14,7 +14,6 @@ import "../lovelace/hui-root";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import { DEFAULT_POWER_COLLECTION_KEY } from "./constants";
@customElement("ha-panel-energy")
class PanelEnergy extends LitElement {
@@ -87,15 +86,7 @@ class PanelEnergy extends LitElement {
private async _setLovelace() {
const config: LovelaceConfig = await generateLovelaceDashboardStrategy(
{
strategy: {
type: "energy",
default_collection:
this.route?.path === "/now"
? DEFAULT_POWER_COLLECTION_KEY
: undefined,
},
},
{ strategy: { type: "energy" } },
this.hass
);
@@ -1,9 +1,6 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
EMPTY_PREFERENCES,
getEnergyDataCollection,
} from "../../../data/energy";
import { getEnergyDataCollection } from "../../../data/energy";
import type { EnergyPreferences } from "../../../data/energy";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { LovelaceConfig } from "../../../data/lovelace/config/types";
@@ -61,9 +58,14 @@ const WIZARD_VIEW = {
cards: [{ type: "custom:energy-setup-wizard-card" }],
};
const EMPTY_PREFERENCES: EnergyPreferences = {
energy_sources: [],
device_consumption: [],
device_consumption_water: [],
};
export interface EnergyDashboardStrategyConfig extends LovelaceStrategyConfig {
type: "energy";
default_collection?: string;
}
@customElement("energy-dashboard-strategy")
@@ -72,7 +74,7 @@ export class EnergyDashboardStrategy extends ReactiveElement {
_config: EnergyDashboardStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceConfig> {
const prefs = await fetchEnergyPrefs(hass, _config.default_collection);
const prefs = await fetchEnergyPrefs(hass);
if (
!prefs ||
@@ -145,19 +147,20 @@ export class EnergyDashboardStrategy extends ReactiveElement {
}
async function fetchEnergyPrefs(
hass: HomeAssistant,
defaultCollection?: string
hass: HomeAssistant
): Promise<EnergyPreferences> {
const collection = getEnergyDataCollection(hass, {
key: defaultCollection || DEFAULT_ENERGY_COLLECTION_KEY,
});
return await new Promise<EnergyPreferences>((resolve) => {
const unsub = collection.subscribe((data) => {
unsub();
resolve(data.prefs || EMPTY_PREFERENCES);
});
key: DEFAULT_ENERGY_COLLECTION_KEY,
});
try {
await collection.refresh();
} catch (err: any) {
if (err.code === "not_found") {
return EMPTY_PREFERENCES;
}
throw err;
}
return collection.prefs || EMPTY_PREFERENCES;
}
declare global {
@@ -21,9 +21,7 @@ export class PowerViewStrategy extends ReactiveElement {
const energyCollection = getEnergyDataCollection(hass, {
key: collectionKey,
});
if (!energyCollection.prefs) {
await energyCollection.refresh();
}
await energyCollection.refresh();
const prefs = energyCollection.prefs;
const hasPowerSources = prefs?.energy_sources.some((source) => {
@@ -438,7 +438,9 @@ class HuiEnergySankeyCard
}
private _valueFormatter = (value: number) =>
`${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)} kWh`;
`<div style="direction:ltr; display: inline;">
${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)}
kWh</div>`;
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
const { node } = ev.detail;
@@ -580,7 +580,9 @@ class HuiPowerSankeyCard
}
private _valueFormatter = (value: number) =>
formatPowerShort(this.hass, value);
`<div style="direction:ltr; display: inline;">
${formatPowerShort(this.hass, value)}
</div>`;
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
const { node } = ev.detail;
@@ -511,11 +511,9 @@ class HuiWaterFlowSankeyCard
}
private _valueFormatter = (value: number) =>
formatFlowRateShort(
this.hass.locale,
this.hass.config.unit_system.length,
value
);
`<div style="direction:ltr; display: inline;">
${formatFlowRateShort(this.hass.locale, this.hass.config.unit_system.length, value)}
</div>`;
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
const { node } = ev.detail;
@@ -30,7 +30,6 @@ import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import "../../../../components/ha-yaml-editor";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
@@ -231,28 +230,11 @@ export class HaCardConditionEditor extends LitElement {
return html`
<div class="container">
<ha-expansion-panel left-chevron>
<div
id="condition-icon"
class="icon-badge-wrapper"
<ha-svg-icon
slot="leading-icon"
>
<ha-svg-icon
.path=${ICON_CONDITION[condition.condition]}
></ha-svg-icon>
${hideLiveTest
? nothing
: html`<ha-automation-row-live-test
.state=${this._liveTestResult.state}
.label=${this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
)}
></ha-automation-row-live-test>`}
</div>
${!hideLiveTest && this._liveTestResult.message
? html`<ha-tooltip for="condition-icon" slot="leading-icon"
>${this._liveTestResult.message}</ha-tooltip
>`
: nothing}
class="condition-icon"
.path=${ICON_CONDITION[condition.condition]}
></ha-svg-icon>
<h3 slot="header">
${this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.condition.${condition.condition}.label`
@@ -273,6 +255,18 @@ export class HaCardConditionEditor extends LitElement {
"ui.panel.lovelace.editor.condition-editor.testing_error"
)}
</ha-automation-row-event-chip>
${hideLiveTest
? nothing
: html`
<ha-automation-row-live-test
slot="icons"
.state=${this._liveTestResult.state}
.label=${this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
)}
.message=${this._liveTestResult.message}
></ha-automation-row-live-test>
`}
<ha-dropdown
slot="icons"
@wa-select=${this._handleAction}
@@ -485,15 +479,17 @@ export class HaCardConditionEditor extends LitElement {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
.icon-badge-wrapper {
.condition-icon {
display: none;
}
@media (min-width: 870px) {
.icon-badge-wrapper {
display: inline-flex;
position: relative;
.condition-icon {
display: inline-block;
color: var(--secondary-text-color);
opacity: 0.9;
margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
}
}
h3 {
@@ -98,6 +98,7 @@ class HaProfileSectionGeneral extends LitElement {
<hass-tabs-subpage
main-page
.hass=${this.hass}
.narrow=${this.narrow}
.tabs=${profileSections}
.route=${this.route}
>
@@ -15,6 +15,8 @@ import "./ha-refresh-tokens-card";
class HaProfileSectionSecurity extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@state() private _refreshTokens?: RefreshToken[];
@property({ attribute: false }) public route!: Route;
@@ -35,6 +37,7 @@ class HaProfileSectionSecurity extends LitElement {
<hass-tabs-subpage
main-page
.hass=${this.hass}
.narrow=${this.narrow}
.tabs=${profileSections}
.route=${this.route}
>
+1 -1
View File
@@ -11213,7 +11213,7 @@
},
"landing-page": {
"header": "Preparing Home Assistant",
"subheader": "This may take 20 minutes or more",
"subheader": "The latest version of Home Assistant is being downloaded. This may take 20 minutes or more.",
"show_details": "Show details",
"hide_details": "Hide details",
"network_issue": {
+12 -12
View File
@@ -8557,7 +8557,7 @@ __metadata:
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
leaflet.markercluster: "npm:1.5.3"
license-checker-rseidelsohn: "npm:5.0.1"
lint-staged: "npm:17.0.6"
lint-staged: "npm:17.0.5"
lit: "npm:3.3.3"
lit-analyzer: "npm:2.0.3"
lit-html: "npm:3.3.3"
@@ -9936,21 +9936,21 @@ __metadata:
languageName: node
linkType: hard
"lint-staged@npm:17.0.6":
version: 17.0.6
resolution: "lint-staged@npm:17.0.6"
"lint-staged@npm:17.0.5":
version: 17.0.5
resolution: "lint-staged@npm:17.0.5"
dependencies:
listr2: "npm:^10.2.1"
picomatch: "npm:^4.0.4"
string-argv: "npm:^0.3.2"
tinyexec: "npm:1.2.2"
yaml: "npm:^2.9.0"
tinyexec: "npm:^1.1.2"
yaml: "npm:^2.8.4"
dependenciesMeta:
yaml:
optional: true
bin:
lint-staged: bin/lint-staged.js
checksum: 10/371918cfb293ed0ca5d16fc2a1de304b5a95d21b87dc1ea7f3751567c8f8a07971a40349fac8edb5fce3c6ea6713f70922ea90184b142fd432a5bb4db6c316b0
checksum: 10/a0bea43689d68ec0bf6a56943884dbdb96b6b49e2677bf80654d802678b2edf9fc65338ca8ef3fc310f245933ea2a809db1ac94431dc445c57a4d49620d9d4da
languageName: node
linkType: hard
@@ -13172,10 +13172,10 @@ __metadata:
languageName: node
linkType: hard
"tinyexec@npm:1.2.2, tinyexec@npm:^1.0.2":
version: 1.2.2
resolution: "tinyexec@npm:1.2.2"
checksum: 10/e6f3cabafc33a46063868b4e9c0ab76722e21cb46f0177f7bef5a9656e09ea6fa37115d3e47f776aff11aab9ab696b0c840c8e0099fab574b1e37767c4371aec
"tinyexec@npm:^1.0.2, tinyexec@npm:^1.1.2":
version: 1.1.2
resolution: "tinyexec@npm:1.1.2"
checksum: 10/2bbe37f9001c6f5723ab39eb8dc1e88f77e830d7cf2e8f34bb75019eb505fcfe3b061b4799c502ff31fa63aa1a9adc649add5ff1e17b7fbd8c16e1afb75d0b9e
languageName: node
linkType: hard
@@ -14755,7 +14755,7 @@ __metadata:
languageName: node
linkType: hard
"yaml@npm:^2.9.0":
"yaml@npm:^2.8.4":
version: 2.9.0
resolution: "yaml@npm:2.9.0"
bin: